diff --git a/MealPlan.Api/Controllers/DietController.cs b/MealPlan.Api/Controllers/DietController.cs new file mode 100644 index 0000000..d9471a6 --- /dev/null +++ b/MealPlan.Api/Controllers/DietController.cs @@ -0,0 +1,284 @@ +using MealPlan.Api.Extensions; +using MealPlan.Api.DTOs.Diet; +using MealPlan.Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +[ApiController] +[Route("api/diet")] +[Authorize] +public class DietController : ControllerBase +{ + private readonly IDietTrackingService _dietService; + + public DietController(IDietTrackingService dietService) => _dietService = dietService; + + /// Daily intake summary with consumed totals, goals, and remaining amounts. + [HttpGet("summary")] + [ProducesResponseType(typeof(DailySummaryDto), StatusCodes.Status200OK)] + public async Task GetSummary([FromQuery] DateOnly? date, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var summary = await _dietService.GetDailySummaryAsync(userId, targetDate, ct); + return Ok(summary); + } + + [HttpGet("goals")] + [ProducesResponseType(typeof(UserDailyGoalsDto), StatusCodes.Status200OK)] + public async Task GetGoals(CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var goals = await _dietService.GetGoalsAsync(userId, ct); + return Ok(goals ?? new UserDailyGoalsDto()); + } + + [HttpPut("goals")] + [ProducesResponseType(typeof(UserDailyGoalsDto), StatusCodes.Status200OK)] + public async Task UpsertGoals([FromBody] UserDailyGoalsDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var goals = await _dietService.UpsertGoalsAsync(userId, dto, ct); + return Ok(goals); + } + + [HttpGet("meals")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetMeals([FromQuery] DateOnly? date, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var meals = await _dietService.GetMealsAsync(userId, targetDate, ct); + return Ok(meals); + } + + [HttpPost("meals")] + [ProducesResponseType(typeof(MealConsumptionDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task LogMeal([FromBody] LogMealDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var meal = await _dietService.LogMealAsync(userId, dto, ct); + if (meal is null) + { + return dto.RecipeId.HasValue + ? NotFound(new { error = $"Recipe {dto.RecipeId} was not found." }) + : BadRequest(new { error = "Invalid meal log payload." }); + } + + return CreatedAtAction(nameof(GetMeals), new { date = meal.LogDate }, meal); + } + + [HttpPost("meals/preview")] + [ProducesResponseType(typeof(MealPreviewDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task PreviewMeal([FromQuery] DateOnly? date, [FromBody] LogMealDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var preview = await _dietService.PreviewMealAsync(userId, targetDate, dto, ct); + return preview is null + ? BadRequest(new { error = "Could not preview meal macros." }) + : Ok(preview); + } + + [HttpGet("suggestions")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetSuggestions([FromQuery] DateOnly? date, [FromQuery] int limit = 5, CancellationToken ct = default) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var suggestions = await _dietService.GetMealSuggestionsAsync(userId, targetDate, limit, ct); + return Ok(suggestions); + } + + [HttpDelete("meals/{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteMeal(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _dietService.DeleteMealAsync(userId, id, ct); + return deleted ? NoContent() : NotFound(new { error = $"Meal log {id} was not found." }); + } + + [HttpGet("drinks/catalog")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetDrinkCatalog(CancellationToken ct) + { + var catalog = await _dietService.GetDrinkCatalogAsync(ct); + return Ok(catalog); + } + + [HttpGet("drinks")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetDrinks([FromQuery] DateOnly? date, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var drinks = await _dietService.GetDrinksAsync(userId, targetDate, ct); + return Ok(drinks); + } + + [HttpPost("drinks")] + [ProducesResponseType(typeof(DrinkConsumptionDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task LogDrink([FromBody] LogDrinkDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var drink = await _dietService.LogDrinkAsync(userId, dto, ct); + if (drink is null) + { + return NotFound(new { error = "Drink catalog item was not found." }); + } + + return CreatedAtAction(nameof(GetDrinks), new { date = drink.LogDate }, drink); + } + + [HttpDelete("drinks/{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteDrink(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _dietService.DeleteDrinkAsync(userId, id, ct); + return deleted ? NoContent() : NotFound(new { error = $"Drink log {id} was not found." }); + } + + [HttpGet("reminders")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetReminders(CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var reminders = await _dietService.GetRemindersAsync(userId, ct); + return Ok(reminders); + } + + [HttpGet("reminders/due")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetDueReminders( + [FromQuery] DateOnly? date, + [FromQuery] TimeOnly? time, + CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var due = await _dietService.GetDueRemindersAsync(userId, date, time, ct); + return Ok(due); + } + + [HttpPost("reminders")] + [ProducesResponseType(typeof(DietReminderDto), StatusCodes.Status201Created)] + public async Task CreateReminder([FromBody] CreateDietReminderDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var reminder = await _dietService.CreateReminderAsync(userId, dto, ct); + return CreatedAtAction(nameof(GetReminders), reminder); + } + + [HttpPut("reminders/{id:int}")] + [ProducesResponseType(typeof(DietReminderDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateReminder(int id, [FromBody] UpdateDietReminderDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var reminder = await _dietService.UpdateReminderAsync(userId, id, dto, ct); + return reminder is null + ? NotFound(new { error = $"Reminder {id} was not found." }) + : Ok(reminder); + } + + [HttpDelete("reminders/{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteReminder(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _dietService.DeleteReminderAsync(userId, id, ct); + return deleted ? NoContent() : NotFound(new { error = $"Reminder {id} was not found." }); + } + + [HttpGet("history")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetHistory([FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var history = await _dietService.GetHistoryAsync(userId, from, to, ct); + return Ok(history); + } + + [HttpGet("meals/recent")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetRecentMeals([FromQuery] int limit = 10, CancellationToken ct = default) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var meals = await _dietService.GetRecentMealsAsync(userId, limit, ct); + return Ok(meals); + } + + [HttpPost("meals/copy-yesterday")] + [ProducesResponseType(typeof(CopyMealsResultDto), StatusCodes.Status200OK)] + public async Task CopyYesterdayMeals([FromQuery] DateOnly? date, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var result = await _dietService.CopyYesterdayMealsAsync(userId, targetDate, ct); + return Ok(result); + } + + [HttpGet("favorites/recipes")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetFavoriteRecipes(CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + return Ok(await _dietService.GetFavoriteRecipesAsync(userId, ct)); + } + + [HttpPost("favorites/recipes/{recipeId:int}")] + [ProducesResponseType(typeof(FavoriteRecipeDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task AddFavoriteRecipe(int recipeId, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var fav = await _dietService.AddFavoriteRecipeAsync(userId, recipeId, ct); + return fav is null ? NotFound() : Ok(fav); + } + + [HttpDelete("favorites/recipes/{recipeId:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveFavoriteRecipe(int recipeId, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _dietService.RemoveFavoriteRecipeAsync(userId, recipeId, ct); + return deleted ? NoContent() : NotFound(); + } + + [HttpGet("favorites/catalog")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetFavoriteCatalogItems(CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + return Ok(await _dietService.GetFavoriteCatalogItemsAsync(userId, ct)); + } + + [HttpPost("favorites/catalog/{catalogItemId:int}")] + [ProducesResponseType(typeof(FavoriteCatalogItemDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task AddFavoriteCatalogItem(int catalogItemId, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var fav = await _dietService.AddFavoriteCatalogItemAsync(userId, catalogItemId, ct); + return fav is null ? NotFound() : Ok(fav); + } + + [HttpDelete("favorites/catalog/{catalogItemId:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task RemoveFavoriteCatalogItem(int catalogItemId, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _dietService.RemoveFavoriteCatalogItemAsync(userId, catalogItemId, ct); + return deleted ? NoContent() : NotFound(); + } +} diff --git a/MealPlan.Api/Controllers/DietGeneratorController.cs b/MealPlan.Api/Controllers/DietGeneratorController.cs new file mode 100644 index 0000000..50e90c8 --- /dev/null +++ b/MealPlan.Api/Controllers/DietGeneratorController.cs @@ -0,0 +1,128 @@ +using MealPlan.Api.DTOs.Generators; +using MealPlan.Api.Extensions; +using MealPlan.Api.Services.Generators; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +[ApiController] +[Route("api/diet-generator")] +[Authorize] +public class DietGeneratorController : ControllerBase +{ + private readonly IDietGeneratorService _service; + + public DietGeneratorController(IDietGeneratorService service) => _service = service; + + [HttpGet("profile")] + [ProducesResponseType(typeof(DietUserProfileDto), StatusCodes.Status200OK)] + public async Task GetProfile(CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var profile = await _service.GetProfileAsync(userId, ct); + return Ok(profile ?? new DietUserProfileDto()); + } + + [HttpPut("profile")] + [ProducesResponseType(typeof(DietUserProfileDto), StatusCodes.Status200OK)] + public async Task UpsertProfile([FromBody] DietUserProfileDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + return Ok(await _service.UpsertProfileAsync(userId, dto, ct)); + } + + [HttpPost("sessions")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status201Created)] + public async Task CreateSession([FromBody] CreateDietGeneratorSessionDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var session = await _service.CreateSessionAsync(userId, dto, ct); + return CreatedAtAction(nameof(GetSession), new { id = session.Id }, session); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpGet("sessions/{id:int}")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetSession(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.GetSessionAsync(userId, id, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/propose-macros")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task ProposeMacros(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.ProposeMacrosAsync(userId, id, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/accept-macros")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task AcceptMacros(int id, [FromBody] AcceptMacrosDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.AcceptMacrosAsync(userId, id, dto, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/generate-plan")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task GeneratePlan(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var session = await _service.GeneratePlanAsync(userId, id, ct); + return session is null ? NotFound() : Ok(session); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpPost("sessions/{id:int}/regenerate-meal")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task RegenerateMeal(int id, [FromBody] RegenerateDietMealDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var session = await _service.RegenerateMealAsync(userId, id, dto, ct); + return session is null ? NotFound() : Ok(session); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpPatch("sessions/{id:int}/meals/{mealId:int}")] + [ProducesResponseType(typeof(DietGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task UpdateMeal(int id, int mealId, [FromBody] UpdateDraftMealDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.UpdateDraftMealAsync(userId, id, mealId, dto, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/commit")] + [ProducesResponseType(typeof(CommitDietPlanResultDto), StatusCodes.Status200OK)] + public async Task Commit(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var result = await _service.CommitAsync(userId, id, ct); + return result is null ? NotFound() : Ok(result); + } +} diff --git a/MealPlan.Api/Controllers/IngredientCatalogController.cs b/MealPlan.Api/Controllers/IngredientCatalogController.cs new file mode 100644 index 0000000..4059e8d --- /dev/null +++ b/MealPlan.Api/Controllers/IngredientCatalogController.cs @@ -0,0 +1,76 @@ +using MealPlan.Api.DTOs.IngredientCatalog; +using MealPlan.Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +[ApiController] +[Route("api/ingredient-catalog")] +[Authorize] +public class IngredientCatalogController : ControllerBase +{ + private readonly IIngredientCatalogService _catalogService; + + public IngredientCatalogController(IIngredientCatalogService catalogService) => + _catalogService = catalogService; + + [HttpGet("categories")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetCategories(CancellationToken ct) + { + var categories = await _catalogService.GetCategoriesAsync(ct); + return Ok(categories); + } + + [HttpGet] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetItems( + [FromQuery] string? category, + [FromQuery] string? search, + CancellationToken ct) + { + var items = await _catalogService.GetItemsAsync(category, search, ct); + return Ok(items); + } + + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(IngredientNutritionItemDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetItem(int id, CancellationToken ct) + { + var item = await _catalogService.GetItemByIdAsync(id, ct); + return item is null ? NotFound(new { error = $"Ingredient catalog item {id} was not found." }) : Ok(item); + } + + [HttpPost] + [ProducesResponseType(typeof(IngredientNutritionItemDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task CreateItem([FromBody] UpsertIngredientNutritionItemDto dto, CancellationToken ct) + { + var item = await _catalogService.CreateItemAsync(dto, ct); + return item is null + ? BadRequest(new { error = "Invalid category or payload." }) + : CreatedAtAction(nameof(GetItem), new { id = item.Id }, item); + } + + [HttpPut("{id:int}")] + [ProducesResponseType(typeof(IngredientNutritionItemDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateItem(int id, [FromBody] UpsertIngredientNutritionItemDto dto, CancellationToken ct) + { + var item = await _catalogService.UpdateItemAsync(id, dto, ct); + return item is null + ? NotFound(new { error = $"Ingredient catalog item {id} was not found." }) + : Ok(item); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteItem(int id, CancellationToken ct) + { + var deleted = await _catalogService.DeleteItemAsync(id, ct); + return deleted ? NoContent() : NotFound(new { error = $"Ingredient catalog item {id} was not found." }); + } +} diff --git a/MealPlan.Api/Controllers/MealPlanController.cs b/MealPlan.Api/Controllers/MealPlanController.cs new file mode 100644 index 0000000..771a2d6 --- /dev/null +++ b/MealPlan.Api/Controllers/MealPlanController.cs @@ -0,0 +1,55 @@ +using MealPlan.Api.DTOs.MealPlan; +using MealPlan.Api.Extensions; +using MealPlan.Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +[ApiController] +[Route("api/meal-plan")] +[Authorize] +public class MealPlanController : ControllerBase +{ + private readonly IMealPlanService _mealPlanService; + + public MealPlanController(IMealPlanService mealPlanService) => _mealPlanService = mealPlanService; + + [HttpGet] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetEntries([FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var entries = await _mealPlanService.GetEntriesAsync(userId, from, to, ct); + return Ok(entries); + } + + [HttpPut] + [ProducesResponseType(typeof(MealPlanEntryDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpsertEntry([FromBody] UpsertMealPlanEntryDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var entry = await _mealPlanService.UpsertEntryAsync(userId, dto, ct); + return entry is null ? NotFound(new { error = "Recipe not found." }) : Ok(entry); + } + + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteEntry(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var deleted = await _mealPlanService.DeleteEntryAsync(userId, id, ct); + return deleted ? NoContent() : NotFound(); + } + + [HttpGet("shopping-list")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + public async Task GetShoppingList([FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var items = await _mealPlanService.GetShoppingListAsync(userId, from, to, ct); + return Ok(items); + } +} diff --git a/MealPlan.Api/Controllers/RecipeGeneratorController.cs b/MealPlan.Api/Controllers/RecipeGeneratorController.cs new file mode 100644 index 0000000..7586597 --- /dev/null +++ b/MealPlan.Api/Controllers/RecipeGeneratorController.cs @@ -0,0 +1,115 @@ +using System.Text.Json; +using MealPlan.Api.DTOs.Generators; +using MealPlan.Api.Extensions; +using MealPlan.Api.Services.Generators; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +[ApiController] +[Route("api/recipe-generator")] +[Authorize] +public class RecipeGeneratorController : ControllerBase +{ + private readonly IRecipeGeneratorService _service; + + public RecipeGeneratorController(IRecipeGeneratorService service) => _service = service; + + [HttpPost("sessions")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status201Created)] + public async Task CreateSession([FromBody] RecipeGeneratorConstraintsDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.CreateSessionAsync(userId, dto, ct); + return CreatedAtAction(nameof(GetSession), new { id = session.Id }, session); + } + + [HttpGet("sessions/{id:int}")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetSession(int id, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.GetSessionAsync(userId, id, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/generate")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task Generate(int id, [FromBody] GenerateRecipesRequestDto? request, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var session = await _service.GenerateAsync(userId, id, request, ct); + return session is null ? NotFound() : Ok(session); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } + + [HttpPost("sessions/{id:int}/regenerate")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task Regenerate(int id, [FromBody] RegenerateRecipePartDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var session = await _service.RegeneratePartAsync(userId, id, dto, ct); + return session is null ? NotFound() : Ok(session); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (JsonException) + { + return BadRequest(new { error = "Could not parse AI recipe response. Try again." }); + } + } + + [HttpPut("sessions/{id:int}/draft")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task UpdateDraft(int id, [FromBody] GeneratedRecipeDraftDto dto, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.UpdateDraftAsync(userId, id, dto, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpDelete("sessions/{id:int}/drafts/{draftId}")] + [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] + public async Task RemoveDraft(int id, string draftId, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + var session = await _service.RemoveDraftAsync(userId, id, draftId, ct); + return session is null ? NotFound() : Ok(session); + } + + [HttpPost("sessions/{id:int}/commit")] + [ProducesResponseType(typeof(CommitRecipesResultDto), StatusCodes.Status200OK)] + public async Task Commit(int id, [FromBody] CommitRecipesRequestDto? request, CancellationToken ct) + { + if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; + try + { + var result = await _service.CommitAsync(userId, id, request, ct); + if (result is null) + { + return NotFound(new + { + error = "Nothing to save. The session may have expired, or the selected drafts were not found.", + }); + } + + return Ok(result); + } + catch (InvalidOperationException ex) + { + return BadRequest(new { error = ex.Message }); + } + } +} diff --git a/MealPlan.Api/Controllers/RecipeManagementController.cs b/MealPlan.Api/Controllers/RecipeManagementController.cs new file mode 100644 index 0000000..ca65e49 --- /dev/null +++ b/MealPlan.Api/Controllers/RecipeManagementController.cs @@ -0,0 +1,93 @@ +using MealPlan.Api.DTOs.Recipe; +using MealPlan.Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Controllers; + +/// Endpoints for creating recipes manually and importing from Excel. +[ApiController] +[Route("api/recipes")] +[Authorize] +public class RecipeManagementController : ControllerBase +{ + private readonly IRecipeManagementService _managementService; + private readonly IRecipeExcelService _excelService; + + public RecipeManagementController( + IRecipeManagementService managementService, + IRecipeExcelService excelService) + { + _managementService = managementService; + _excelService = excelService; + } + + /// Creates a new recipe with ingredients and steps. + [HttpPost] + [ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task Create([FromBody] CreateRecipeDto dto, CancellationToken ct) + { + var recipe = await _managementService.CreateRecipeAsync(dto, ct); + return CreatedAtAction( + nameof(RecipesController.GetRecipe), + "Recipes", + new { id = recipe.Id }, + recipe); + } + + /// Updates an existing recipe (replaces ingredients and steps). + [HttpPut("{id:int}")] + [ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Update(int id, [FromBody] CreateRecipeDto dto, CancellationToken ct) + { + var recipe = await _managementService.UpdateRecipeAsync(id, dto, ct); + return recipe is null + ? NotFound(new { error = $"Recipe {id} was not found." }) + : Ok(recipe); + } + + /// Downloads an Excel template for bulk recipe import. + [HttpGet("import/template")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + public IActionResult DownloadTemplate() + { + var bytes = _excelService.GenerateTemplate(); + return File( + bytes, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "DailyMeals-recipe-import-template.xlsx"); + } + + /// Imports recipes from an uploaded .xlsx file (Recipes + Ingredients + Steps sheets). + [HttpPost("import/excel")] + [RequestSizeLimit(10_000_000)] + [ProducesResponseType(typeof(RecipeImportResultDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task ImportExcel(IFormFile? file, CancellationToken ct) + { + if (file is null || file.Length == 0) + { + return BadRequest(new { error = "No file uploaded." }); + } + + if (!file.FileName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)) + { + return BadRequest(new { error = "Only .xlsx files are supported." }); + } + + await using var stream = file.OpenReadStream(); + var result = await _excelService.ImportAsync(stream, ct); + return Ok(result); + } + + /// Recalculates recipe macros from linked catalog ingredients (all active recipes). + [HttpPost("recalculate-macros")] + [ProducesResponseType(typeof(RecipeMacroRecalcResultDto), StatusCodes.Status200OK)] + public async Task RecalculateMacros(CancellationToken ct) + { + var result = await _managementService.RecalculateAllMacrosFromCatalogAsync(ct); + return Ok(result); + } +} diff --git a/MealPlan.Api/Controllers/RecipesController.cs b/MealPlan.Api/Controllers/RecipesController.cs index 74da835..515a5d5 100644 --- a/MealPlan.Api/Controllers/RecipesController.cs +++ b/MealPlan.Api/Controllers/RecipesController.cs @@ -14,12 +14,16 @@ public class RecipesController : ControllerBase public RecipesController(IRecipeService recipeService) => _recipeService = recipeService; - /// Lists active recipes, optionally filtered by category and/or name search. + /// + /// Lists active recipes, optionally filtered by category, recipe-name search, + /// and/or an ingredient name (returns recipes that contain that ingredient). + /// [HttpGet] [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] public async Task GetRecipes( [FromQuery] int? category, [FromQuery] string? search, + [FromQuery] string? ingredient, CancellationToken ct) { if (category is < 0 or > 3) @@ -27,7 +31,7 @@ public class RecipesController : ControllerBase return BadRequest(new { error = "category must be between 0 and 3." }); } - var recipes = await _recipeService.GetRecipesAsync(category, search, ct); + var recipes = await _recipeService.GetRecipesAsync(category, search, ingredient, ct); return Ok(recipes); } diff --git a/MealPlan.Api/DTOs/Diet/DietDtos.cs b/MealPlan.Api/DTOs/Diet/DietDtos.cs new file mode 100644 index 0000000..ac49cbb --- /dev/null +++ b/MealPlan.Api/DTOs/Diet/DietDtos.cs @@ -0,0 +1,227 @@ +namespace MealPlan.Api.DTOs.Diet; + +public class UserDailyGoalsDto +{ + public int? CalorieGoal { get; set; } + public decimal? ProteinGoalG { get; set; } + public decimal? FatGoalG { get; set; } + public decimal? CarbsGoalG { get; set; } + public int? WaterGoalMl { get; set; } +} + +public class MacroRemainingDto +{ + public int? Goal { get; set; } + public int Consumed { get; set; } + public int? Remaining { get; set; } + public decimal? ProgressPercent { get; set; } +} + +public class DecimalMacroRemainingDto +{ + public decimal? Goal { get; set; } + public decimal Consumed { get; set; } + public decimal? Remaining { get; set; } + public decimal? ProgressPercent { get; set; } +} + +public class DailySummaryDto +{ + public DateOnly Date { get; set; } + public UserDailyGoalsDto? Goals { get; set; } + public MacroRemainingDto Calories { get; set; } = new(); + public DecimalMacroRemainingDto ProteinG { get; set; } = new(); + public DecimalMacroRemainingDto FatG { get; set; } = new(); + public DecimalMacroRemainingDto CarbsG { get; set; } = new(); + public MacroRemainingDto WaterMl { get; set; } = new(); + public IReadOnlyList Meals { get; set; } = Array.Empty(); + public IReadOnlyList Drinks { get; set; } = Array.Empty(); +} + +public class MealConsumptionDto +{ + public int Id { get; set; } + public int? RecipeId { get; set; } + public int MealCategory { get; set; } + public DateOnly LogDate { get; set; } + public DateTime ConsumedAt { get; set; } + public string Name { get; set; } = string.Empty; + public decimal Portions { get; set; } + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } + public string? Notes { get; set; } +} + +public class LogMealDto +{ + public int? RecipeId { get; set; } + public int? CatalogItemId { get; set; } + public decimal? Grams { get; set; } + public string? Name { get; set; } + public int MealCategory { get; set; } + public DateOnly? LogDate { get; set; } + public DateTime? ConsumedAt { get; set; } + public decimal Portions { get; set; } = 1m; + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } + public string? Notes { get; set; } +} + +public class MealMacroSnapshotDto +{ + public string Name { get; set; } = string.Empty; + public decimal Portions { get; set; } = 1m; + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } +} + +public class RemainingAfterMealDto +{ + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } +} + +public class MealPreviewDto +{ + public MealMacroSnapshotDto Meal { get; set; } = new(); + public RemainingAfterMealDto RemainingAfter { get; set; } = new(); +} + +public class DietSuggestionDto +{ + public int CatalogItemId { get; set; } + public string Name { get; set; } = string.Empty; + public string? NamePl { get; set; } + public string CategoryCode { get; set; } = string.Empty; + public int SuggestedGrams { get; set; } + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } + public string ReasonKey { get; set; } = string.Empty; +} + +public class DrinkCatalogItemDto +{ + public int Id { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int DefaultVolumeMl { get; set; } + public decimal? CaloriesPer100Ml { get; set; } + public string? IconEmoji { get; set; } +} + +public class DrinkConsumptionDto +{ + public int Id { get; set; } + public int? DrinkCatalogId { get; set; } + public DateOnly LogDate { get; set; } + public DateTime ConsumedAt { get; set; } + public string Name { get; set; } = string.Empty; + public int VolumeMl { get; set; } + public int? Calories { get; set; } +} + +public class LogDrinkDto +{ + public int? DrinkCatalogId { get; set; } + public string? Name { get; set; } + public int VolumeMl { get; set; } + public DateOnly? LogDate { get; set; } + public DateTime? ConsumedAt { get; set; } + public int? Calories { get; set; } +} + +public class DietReminderDto +{ + public int Id { get; set; } + public int ReminderType { get; set; } + public string Title { get; set; } = string.Empty; + public string? Message { get; set; } + public TimeOnly TimeOfDay { get; set; } + public int DaysOfWeekMask { get; set; } + public int? MealCategory { get; set; } + public bool IsEnabled { get; set; } +} + +public class CreateDietReminderDto +{ + public int ReminderType { get; set; } + public string Title { get; set; } = string.Empty; + public string? Message { get; set; } + public TimeOnly TimeOfDay { get; set; } + public int DaysOfWeekMask { get; set; } = Models.ReminderDaysMask.All; + public int? MealCategory { get; set; } + public bool IsEnabled { get; set; } = true; +} + +public class UpdateDietReminderDto : CreateDietReminderDto +{ +} + +public class DueReminderDto +{ + public int Id { get; set; } + public int ReminderType { get; set; } + public string Title { get; set; } = string.Empty; + public string? Message { get; set; } + public TimeOnly TimeOfDay { get; set; } + public int? MealCategory { get; set; } +} + +public class HistoryDayDto +{ + public DateOnly Date { get; set; } + public int Calories { get; set; } + public decimal ProteinG { get; set; } + public decimal FatG { get; set; } + public decimal CarbsG { get; set; } + public int WaterMl { get; set; } + public int MealCount { get; set; } +} + +public class RecentMealDto +{ + public int? RecipeId { get; set; } + public int? CatalogItemId { get; set; } + public string Name { get; set; } = string.Empty; + public int MealCategory { get; set; } + public decimal Portions { get; set; } = 1m; + public decimal? Grams { get; set; } + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } + public DateTime LastLoggedAt { get; set; } +} + +public class FavoriteRecipeDto +{ + public int RecipeId { get; set; } + public string Name { get; set; } = string.Empty; + public int MealCategory { get; set; } + public int? Calories { get; set; } +} + +public class FavoriteCatalogItemDto +{ + public int CatalogItemId { get; set; } + public string Name { get; set; } = string.Empty; + public string? NamePl { get; set; } + public string CategoryCode { get; set; } = string.Empty; +} + +public class CopyMealsResultDto +{ + public DateOnly SourceDate { get; set; } + public DateOnly TargetDate { get; set; } + public int CopiedCount { get; set; } +} diff --git a/MealPlan.Api/DTOs/Generators/GeneratorDtos.cs b/MealPlan.Api/DTOs/Generators/GeneratorDtos.cs new file mode 100644 index 0000000..e2de93e --- /dev/null +++ b/MealPlan.Api/DTOs/Generators/GeneratorDtos.cs @@ -0,0 +1,201 @@ +namespace MealPlan.Api.DTOs.Generators; + +public class DietUserProfileDto +{ + public decimal HeightCm { get; set; } + public decimal WeightKg { get; set; } + public int Age { get; set; } + public string Sex { get; set; } = "male"; + public string ActivityLevel { get; set; } = "moderate"; + public string Goal { get; set; } = "maintain"; + public string? AllergiesNotes { get; set; } + public string? DislikedFoods { get; set; } + public int MealsPerDayMask { get; set; } = 15; +} + +public class CreateDietGeneratorSessionDto +{ + public DateOnly? PlanStartDate { get; set; } + public int PlanDayCount { get; set; } = 7; + public int CalorieToleranceKcal { get; set; } = 10; +} + +public class MacroProposalDto +{ + public int Calories { get; set; } + public decimal ProteinG { get; set; } + public decimal FatG { get; set; } + public decimal CarbsG { get; set; } + public string? Rationale { get; set; } +} + +public class AcceptMacrosDto +{ + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } +} + +public class DietGeneratorSessionDto +{ + public int Id { get; set; } + public string Status { get; set; } = string.Empty; + public MacroProposalDto? ProposedMacros { get; set; } + public DateOnly? PlanStartDate { get; set; } + public int PlanDayCount { get; set; } + public int CalorieToleranceKcal { get; set; } + public IReadOnlyList DraftMeals { get; set; } = Array.Empty(); + public IReadOnlyList DaySummaries { get; set; } = Array.Empty(); +} + +public class DietGeneratorDraftMealDto +{ + public int Id { get; set; } + public DateOnly PlanDate { get; set; } + public int MealCategory { get; set; } + public int RecipeId { get; set; } + public string RecipeName { get; set; } = string.Empty; + public decimal Portions { get; set; } + public int? Calories { get; set; } + public decimal? ProteinG { get; set; } + public decimal? FatG { get; set; } + public decimal? CarbsG { get; set; } + public bool IsLocked { get; set; } + public int? PrepTimeMinutes { get; set; } + public IReadOnlyList Ingredients { get; set; } = + Array.Empty(); + public IReadOnlyList Steps { get; set; } = + Array.Empty(); +} + +public class DietGeneratorDraftMealStepDto +{ + public int Id { get; set; } + public int StepNumber { get; set; } + public string Description { get; set; } = string.Empty; +} + +public class DietGeneratorDraftMealIngredientDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public decimal? AmountGrams { get; set; } + public string? Unit { get; set; } + public int SortOrder { get; set; } + public int? SourceIngredientId { get; set; } +} + +public class DietGeneratorDaySummaryDto +{ + public DateOnly PlanDate { get; set; } + public int TotalCalories { get; set; } + public int TargetCalories { get; set; } + public bool WithinTolerance { get; set; } +} + +public class RegenerateDietMealDto +{ + public DateOnly PlanDate { get; set; } + public int MealCategory { get; set; } +} + +public class UpdateDraftMealDto +{ + public int? RecipeId { get; set; } + public decimal? Portions { get; set; } + public bool? IsLocked { get; set; } +} + +public class CommitDietPlanResultDto +{ + public int SessionId { get; set; } + public DateOnly PlanStartDate { get; set; } + public DateOnly PlanEndDate { get; set; } + public int MealsWritten { get; set; } +} + +public class RecipeGeneratorConstraintsDto +{ + public string Prompt { get; set; } = string.Empty; + public int MealCategory { get; set; } + public int? TargetCalories { get; set; } + public int CalorieToleranceKcal { get; set; } = 10; + public int? MaxPrepTimeMinutes { get; set; } + public string? CuisineStyle { get; set; } + public string? DietStyle { get; set; } + public string? Exclusions { get; set; } +} + +public class GeneratedIngredientDto +{ + public string Name { get; set; } = string.Empty; + public decimal? AmountGrams { get; set; } + public string? Unit { get; set; } + public int? CatalogItemId { get; set; } + public string? CatalogName { get; set; } + public bool IsLinked { get; set; } +} + +public class GeneratedStepDto +{ + public int StepNumber { get; set; } + public string Description { get; set; } = string.Empty; +} + +public class GeneratedRecipeDraftDto +{ + public string DraftId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int MealCategory { get; set; } + public int? PrepTimeMinutes { get; set; } + public int? Calories { get; set; } + public decimal? Protein { get; set; } + public decimal? Fat { get; set; } + public decimal? Carbs { get; set; } + public IReadOnlyList Ingredients { get; set; } = Array.Empty(); + public IReadOnlyList Steps { get; set; } = Array.Empty(); + public int UnlinkedIngredientCount { get; set; } +} + +public class RecipeGeneratorSessionDto +{ + public int Id { get; set; } + public string Status { get; set; } = string.Empty; + public RecipeGeneratorConstraintsDto Constraints { get; set; } = new(); + public GeneratedRecipeDraftDto? Draft { get; set; } + public IReadOnlyList Drafts { get; set; } = Array.Empty(); + public int? SavedRecipeId { get; set; } + public int GenerationVersion { get; set; } +} + +public class GenerateRecipesRequestDto +{ + public int Count { get; set; } = 3; +} + +public class CommitRecipesRequestDto +{ + public IReadOnlyList? DraftIds { get; set; } +} + +public class RegenerateRecipePartDto +{ + public string DraftId { get; set; } = string.Empty; + public string Mode { get; set; } = "ingredients"; + public string? Instruction { get; set; } +} + +public class CommitRecipeResultDto +{ + public int SessionId { get; set; } + public int RecipeId { get; set; } + public string RecipeName { get; set; } = string.Empty; + public string DraftId { get; set; } = string.Empty; +} + +public class CommitRecipesResultDto +{ + public int SessionId { get; set; } + public IReadOnlyList Saved { get; set; } = Array.Empty(); +} diff --git a/MealPlan.Api/DTOs/IngredientCatalog/IngredientCatalogDtos.cs b/MealPlan.Api/DTOs/IngredientCatalog/IngredientCatalogDtos.cs new file mode 100644 index 0000000..8934c2e --- /dev/null +++ b/MealPlan.Api/DTOs/IngredientCatalog/IngredientCatalogDtos.cs @@ -0,0 +1,38 @@ +namespace MealPlan.Api.DTOs.IngredientCatalog; + +public class IngredientCategoryDto +{ + public int Id { get; set; } + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public int ItemCount { get; set; } +} + +public class IngredientNutritionItemDto +{ + public int Id { get; set; } + public int CategoryId { get; set; } + public string CategoryCode { get; set; } = string.Empty; + public string CategoryName { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string? NamePl { get; set; } + public int CaloriesPer100G { get; set; } + public decimal ProteinGPer100G { get; set; } + public decimal FatGPer100G { get; set; } + public decimal CarbsGPer100G { get; set; } + public decimal? FiberGPer100G { get; set; } +} + +public class UpsertIngredientNutritionItemDto +{ + public int CategoryId { get; set; } + public string Name { get; set; } = string.Empty; + public string? NamePl { get; set; } + public int CaloriesPer100G { get; set; } + public decimal ProteinGPer100G { get; set; } + public decimal FatGPer100G { get; set; } + public decimal CarbsGPer100G { get; set; } + public decimal? FiberGPer100G { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; +} diff --git a/MealPlan.Api/DTOs/MealPlan/MealPlanDtos.cs b/MealPlan.Api/DTOs/MealPlan/MealPlanDtos.cs new file mode 100644 index 0000000..bc6b614 --- /dev/null +++ b/MealPlan.Api/DTOs/MealPlan/MealPlanDtos.cs @@ -0,0 +1,29 @@ +namespace MealPlan.Api.DTOs.MealPlan; + +public class MealPlanEntryDto +{ + public int Id { get; set; } + public DateOnly PlanDate { get; set; } + public int MealCategory { get; set; } + public int RecipeId { get; set; } + public string RecipeName { get; set; } = string.Empty; + public decimal Portions { get; set; } + public int? Calories { get; set; } +} + +public class UpsertMealPlanEntryDto +{ + public DateOnly PlanDate { get; set; } + public int MealCategory { get; set; } + public int RecipeId { get; set; } + public decimal Portions { get; set; } = 1m; +} + +public class ShoppingListItemDto +{ + public string Name { get; set; } = string.Empty; + public string? Unit { get; set; } + public decimal? TotalGrams { get; set; } + public decimal? TotalAmount { get; set; } + public IReadOnlyList Breakdown { get; set; } = Array.Empty(); +} diff --git a/MealPlan.Api/DTOs/Recipe/CreateRecipeDto.cs b/MealPlan.Api/DTOs/Recipe/CreateRecipeDto.cs new file mode 100644 index 0000000..d540b44 --- /dev/null +++ b/MealPlan.Api/DTOs/Recipe/CreateRecipeDto.cs @@ -0,0 +1,59 @@ +using System.ComponentModel.DataAnnotations; + +namespace MealPlan.Api.DTOs.Recipe; + +/// Payload for creating or updating a recipe with nested ingredients and steps. +public class CreateRecipeDto +{ + [Required] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + /// 0 = Breakfast, 1 = SecondBreakfast, 2 = Lunch, 3 = Dinner. + [Range(0, 3)] + public int MealCategory { get; set; } + + [Range(0, 100000)] + public int? Calories { get; set; } + + [Range(0, 10000)] + public decimal? Protein { get; set; } + + [Range(0, 10000)] + public decimal? Fat { get; set; } + + [Range(0, 10000)] + public decimal? Carbs { get; set; } + + [Range(0, 100000)] + public int? PrepTimeMinutes { get; set; } + + public List Ingredients { get; set; } = new(); + public List Steps { get; set; } = new(); +} + +public class CreateIngredientDto +{ + [Required] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [Range(0, 100000)] + public decimal? AmountGrams { get; set; } + + [MaxLength(50)] + public string? Unit { get; set; } + + public int? CatalogItemId { get; set; } + + public int SortOrder { get; set; } +} + +public class CreateRecipeStepDto +{ + [Range(1, 1000)] + public int StepNumber { get; set; } + + [Required] + public string Description { get; set; } = string.Empty; +} diff --git a/MealPlan.Api/DTOs/Recipe/RecipeDetailDto.cs b/MealPlan.Api/DTOs/Recipe/RecipeDetailDto.cs index 1693b25..0edb241 100644 --- a/MealPlan.Api/DTOs/Recipe/RecipeDetailDto.cs +++ b/MealPlan.Api/DTOs/Recipe/RecipeDetailDto.cs @@ -23,6 +23,7 @@ public class IngredientDto public decimal? AmountGrams { get; set; } public string? Unit { get; set; } public int SortOrder { get; set; } + public int? CatalogItemId { get; set; } } public class RecipeStepDto diff --git a/MealPlan.Api/DTOs/Recipe/RecipeImportResultDto.cs b/MealPlan.Api/DTOs/Recipe/RecipeImportResultDto.cs new file mode 100644 index 0000000..12582ff --- /dev/null +++ b/MealPlan.Api/DTOs/Recipe/RecipeImportResultDto.cs @@ -0,0 +1,10 @@ +namespace MealPlan.Api.DTOs.Recipe; + +/// Summary returned after an Excel import operation. +public class RecipeImportResultDto +{ + public int CreatedCount { get; set; } + public int SkippedCount { get; set; } + public List Errors { get; set; } = new(); + public List CreatedRecipeIds { get; set; } = new(); +} diff --git a/MealPlan.Api/DTOs/Recipe/RecipeMacroRecalcResultDto.cs b/MealPlan.Api/DTOs/Recipe/RecipeMacroRecalcResultDto.cs new file mode 100644 index 0000000..e98174e --- /dev/null +++ b/MealPlan.Api/DTOs/Recipe/RecipeMacroRecalcResultDto.cs @@ -0,0 +1,10 @@ +namespace MealPlan.Api.DTOs.Recipe; + +public class RecipeMacroRecalcResultDto +{ + public int RecipesProcessed { get; init; } + + public int RecipesUpdated { get; init; } + + public int UnlinkedIngredientRows { get; init; } +} diff --git a/MealPlan.Api/Data/AppDbContext.cs b/MealPlan.Api/Data/AppDbContext.cs index 9f8602c..e8d3ae5 100644 --- a/MealPlan.Api/Data/AppDbContext.cs +++ b/MealPlan.Api/Data/AppDbContext.cs @@ -17,6 +17,23 @@ public class AppDbContext : DbContext public DbSet Ingredients => Set(); public DbSet RecipeSteps => Set(); public DbSet Users => Set(); + public DbSet UserDailyGoals => Set(); + public DbSet DrinkCatalog => Set(); + public DbSet MealConsumptions => Set(); + public DbSet DrinkConsumptions => Set(); + public DbSet DietReminders => Set(); + public DbSet IngredientCategories => Set(); + public DbSet IngredientNutritionCatalog => Set(); + public DbSet RefreshTokens => Set(); + public DbSet MealPlanEntries => Set(); + public DbSet UserFavoriteRecipes => Set(); + public DbSet UserFavoriteCatalogItems => Set(); + public DbSet DietUserProfiles => Set(); + public DbSet DietGeneratorSessions => Set(); + public DbSet DietGeneratorDraftMeals => Set(); + public DbSet DietGeneratorDraftMealIngredients => + Set(); + public DbSet RecipeGeneratorSessions => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -48,6 +65,10 @@ public class AppDbContext : DbContext e.HasKey(i => i.Id); e.Property(i => i.Id).UseIdentityByDefaultColumn(); e.Property(i => i.AmountGrams).HasColumnType("numeric(8,2)"); + e.HasOne(i => i.CatalogItem) + .WithMany() + .HasForeignKey(i => i.CatalogItemId) + .OnDelete(DeleteBehavior.SetNull); }); modelBuilder.Entity(e => @@ -61,5 +82,180 @@ public class AppDbContext : DbContext e.HasKey(u => u.Id); e.Property(u => u.Id).HasColumnType("uuid"); }); + + modelBuilder.Entity(e => + { + e.HasKey(g => g.UserId); + e.Property(g => g.UserId).HasColumnType("uuid"); + e.Property(g => g.ProteinGoalG).HasColumnType("numeric(6,2)"); + e.Property(g => g.FatGoalG).HasColumnType("numeric(6,2)"); + e.Property(g => g.CarbsGoalG).HasColumnType("numeric(6,2)"); + e.HasOne(g => g.User) + .WithMany() + .HasForeignKey(g => g.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(d => d.Id); + e.Property(d => d.Id).UseIdentityByDefaultColumn(); + e.Property(d => d.CaloriesPer100Ml).HasColumnType("numeric(6,2)"); + }); + + modelBuilder.Entity(e => + { + e.HasKey(m => m.Id); + e.Property(m => m.Id).UseIdentityByDefaultColumn(); + e.Property(m => m.UserId).HasColumnType("uuid"); + e.Property(m => m.Portions).HasColumnType("numeric(4,2)"); + e.Property(m => m.ProteinG).HasColumnType("numeric(6,2)"); + e.Property(m => m.FatG).HasColumnType("numeric(6,2)"); + e.Property(m => m.CarbsG).HasColumnType("numeric(6,2)"); + e.HasOne(m => m.User) + .WithMany() + .HasForeignKey(m => m.UserId) + .OnDelete(DeleteBehavior.Cascade); + e.HasOne(m => m.Recipe) + .WithMany() + .HasForeignKey(m => m.RecipeId) + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity(e => + { + e.HasKey(d => d.Id); + e.Property(d => d.Id).UseIdentityByDefaultColumn(); + e.Property(d => d.UserId).HasColumnType("uuid"); + e.HasOne(d => d.User) + .WithMany() + .HasForeignKey(d => d.UserId) + .OnDelete(DeleteBehavior.Cascade); + e.HasOne(d => d.DrinkCatalog) + .WithMany() + .HasForeignKey(d => d.DrinkCatalogId) + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity(e => + { + e.HasKey(r => r.Id); + e.Property(r => r.Id).UseIdentityByDefaultColumn(); + e.Property(r => r.UserId).HasColumnType("uuid"); + e.HasOne(r => r.User) + .WithMany() + .HasForeignKey(r => r.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(c => c.Id); + e.Property(c => c.Id).UseIdentityByDefaultColumn(); + }); + + modelBuilder.Entity(e => + { + e.HasKey(i => i.Id); + e.Property(i => i.Id).UseIdentityByDefaultColumn(); + e.Property(i => i.ProteinGPer100G).HasColumnType("numeric(6,2)"); + e.Property(i => i.FatGPer100G).HasColumnType("numeric(6,2)"); + e.Property(i => i.CarbsGPer100G).HasColumnType("numeric(6,2)"); + e.Property(i => i.FiberGPer100G).HasColumnType("numeric(6,2)"); + e.HasOne(i => i.Category) + .WithMany(c => c.Items) + .HasForeignKey(i => i.CategoryId) + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity(e => + { + e.HasKey(t => t.Token); + e.Property(t => t.UserId).HasColumnType("uuid"); + e.HasOne(t => t.User) + .WithMany() + .HasForeignKey(t => t.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(m => m.Id); + e.Property(m => m.Id).UseIdentityByDefaultColumn(); + e.Property(m => m.UserId).HasColumnType("uuid"); + e.Property(m => m.Portions).HasColumnType("numeric(4,2)"); + e.HasOne(m => m.User).WithMany().HasForeignKey(m => m.UserId).OnDelete(DeleteBehavior.Cascade); + e.HasOne(m => m.Recipe).WithMany().HasForeignKey(m => m.RecipeId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(f => new { f.UserId, f.RecipeId }); + e.Property(f => f.UserId).HasColumnType("uuid"); + e.HasOne(f => f.Recipe).WithMany().HasForeignKey(f => f.RecipeId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(f => new { f.UserId, f.CatalogItemId }); + e.Property(f => f.UserId).HasColumnType("uuid"); + e.HasOne(f => f.CatalogItem).WithMany().HasForeignKey(f => f.CatalogItemId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(p => p.UserId); + e.Property(p => p.UserId).HasColumnType("uuid"); + e.Property(p => p.HeightCm).HasColumnType("numeric(5,2)"); + e.Property(p => p.WeightKg).HasColumnType("numeric(5,2)"); + e.HasOne(p => p.User) + .WithMany() + .HasForeignKey(p => p.UserId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(s => s.Id); + e.Property(s => s.Id).UseIdentityByDefaultColumn(); + e.Property(s => s.UserId).HasColumnType("uuid"); + e.Property(s => s.ProposedProteinG).HasColumnType("numeric(6,2)"); + e.Property(s => s.ProposedFatG).HasColumnType("numeric(6,2)"); + e.Property(s => s.ProposedCarbsG).HasColumnType("numeric(6,2)"); + e.Property(s => s.PreferencesJson).HasColumnType("jsonb"); + e.HasMany(s => s.DraftMeals).WithOne(m => m.Session!).HasForeignKey(m => m.SessionId).OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(m => m.Id); + e.Property(m => m.Id).UseIdentityByDefaultColumn(); + e.Property(m => m.Portions).HasColumnType("numeric(4,2)"); + e.Property(m => m.ProteinG).HasColumnType("numeric(6,2)"); + e.Property(m => m.FatG).HasColumnType("numeric(6,2)"); + e.Property(m => m.CarbsG).HasColumnType("numeric(6,2)"); + e.HasOne(m => m.Recipe).WithMany().HasForeignKey(m => m.RecipeId).OnDelete(DeleteBehavior.Cascade); + e.HasMany(m => m.PlannedIngredients).WithOne(i => i.DraftMeal!).HasForeignKey(i => i.DraftMealId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(e => + { + e.HasKey(i => i.Id); + e.Property(i => i.Id).UseIdentityByDefaultColumn(); + e.Property(i => i.AmountGrams).HasColumnType("numeric(8,2)"); + e.HasOne(i => i.SourceIngredient).WithMany().HasForeignKey(i => i.SourceIngredientId) + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity(e => + { + e.HasKey(s => s.Id); + e.Property(s => s.Id).UseIdentityByDefaultColumn(); + e.Property(s => s.UserId).HasColumnType("uuid"); + e.Property(s => s.ConstraintsJson).HasColumnType("jsonb"); + e.Property(s => s.DraftJson).HasColumnType("jsonb"); + e.HasOne(s => s.SavedRecipe).WithMany().HasForeignKey(s => s.SavedRecipeId).OnDelete(DeleteBehavior.SetNull); + }); } } diff --git a/MealPlan.Api/Data/SequenceMaintenance.cs b/MealPlan.Api/Data/SequenceMaintenance.cs new file mode 100644 index 0000000..e8b0742 --- /dev/null +++ b/MealPlan.Api/Data/SequenceMaintenance.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Data; + +/// +/// Resyncs PostgreSQL identity sequences after bulk imports or manual ID inserts. +/// +public static class SequenceMaintenance +{ + private static readonly (string Table, string Column)[] IdentityTables = + [ + ("Recipes", "Id"), + ("Ingredients", "Id"), + ("RecipeSteps", "Id"), + ("RecipeGeneratorSessions", "Id"), + ("DietGeneratorSessions", "Id"), + ("DietGeneratorDraftMeals", "Id"), + ("DietGeneratorDraftMealIngredients", "Id"), + ("MealPlanEntries", "Id"), + ]; + + public static async Task> ResyncIdentitySequencesAsync( + AppDbContext db, + CancellationToken ct = default) + { + var messages = new List(); + + foreach (var (table, column) in IdentityTables) + { + var sql = $""" + SELECT setval( + pg_get_serial_sequence('diet."{table}"', '{column}'), + COALESCE((SELECT MAX("{column}") FROM diet."{table}"), 1), + (SELECT MAX("{column}") IS NOT NULL FROM diet."{table}")) + """; + + try + { + await db.Database.ExecuteSqlRawAsync(sql, ct); + messages.Add($"Resynced diet.\"{table}\".\"{column}\" sequence."); + } + catch (Exception ex) + { + messages.Add($"Skipped diet.\"{table}\": {ex.Message}"); + } + } + + return messages; + } +} diff --git a/MealPlan.Api/Extensions/ClaimsPrincipalExtensions.cs b/MealPlan.Api/Extensions/ClaimsPrincipalExtensions.cs new file mode 100644 index 0000000..7fa1498 --- /dev/null +++ b/MealPlan.Api/Extensions/ClaimsPrincipalExtensions.cs @@ -0,0 +1,27 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; + +namespace MealPlan.Api.Extensions; + +public static class ClaimsPrincipalExtensions +{ + public static Guid? GetUserId(this ClaimsPrincipal user) + { + var raw = user.FindFirstValue(ClaimTypes.NameIdentifier) + ?? user.FindFirstValue("sub"); + return Guid.TryParse(raw, out var id) ? id : null; + } + + public static IActionResult? RequireUserId(this ClaimsPrincipal user, out Guid userId) + { + var id = user.GetUserId(); + if (id is null) + { + userId = Guid.Empty; + return new UnauthorizedResult(); + } + + userId = id.Value; + return null; + } +} diff --git a/MealPlan.Api/MealPlan.Api.csproj b/MealPlan.Api/MealPlan.Api.csproj index 13c31d3..1915762 100644 --- a/MealPlan.Api/MealPlan.Api.csproj +++ b/MealPlan.Api/MealPlan.Api.csproj @@ -10,6 +10,7 @@ + diff --git a/MealPlan.Api/Models/DietEnums.cs b/MealPlan.Api/Models/DietEnums.cs new file mode 100644 index 0000000..79ebbf2 --- /dev/null +++ b/MealPlan.Api/Models/DietEnums.cs @@ -0,0 +1,41 @@ +namespace MealPlan.Api.Models; + +/// Meal slot when logging consumption. Extends recipe categories with snack. +public enum ConsumptionMealCategory +{ + Breakfast = 0, + SecondBreakfast = 1, + Lunch = 2, + Dinner = 3, + Snack = 4, +} + +public enum DietReminderType +{ + Water = 0, + Meal = 1, + Snack = 2, + Custom = 3, +} + +/// Bitmask for reminder days: Sun=1, Mon=2, Tue=4, Wed=8, Thu=16, Fri=32, Sat=64. +public static class ReminderDaysMask +{ + public const int All = 127; + + public static bool IncludesDay(int mask, DayOfWeek day) + { + var bit = day switch + { + DayOfWeek.Sunday => 1, + DayOfWeek.Monday => 2, + DayOfWeek.Tuesday => 4, + DayOfWeek.Wednesday => 8, + DayOfWeek.Thursday => 16, + DayOfWeek.Friday => 32, + DayOfWeek.Saturday => 64, + _ => 0, + }; + return (mask & bit) != 0; + } +} diff --git a/MealPlan.Api/Models/DietGeneratorDraftMeal.cs b/MealPlan.Api/Models/DietGeneratorDraftMeal.cs new file mode 100644 index 0000000..383c859 --- /dev/null +++ b/MealPlan.Api/Models/DietGeneratorDraftMeal.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DietGeneratorDraftMeals")] +public class DietGeneratorDraftMeal +{ + [Column("Id")] + public int Id { get; set; } + + [Column("SessionId")] + public int SessionId { get; set; } + + [Column("PlanDate")] + public DateOnly PlanDate { get; set; } + + [Column("MealCategory")] + public int MealCategory { get; set; } + + [Column("RecipeId")] + public int RecipeId { get; set; } + + [Column("Portions")] + public decimal Portions { get; set; } = 1; + + [Column("Calories")] + public int? Calories { get; set; } + + [Column("ProteinG")] + public decimal? ProteinG { get; set; } + + [Column("FatG")] + public decimal? FatG { get; set; } + + [Column("CarbsG")] + public decimal? CarbsG { get; set; } + + [Column("IsLocked")] + public bool IsLocked { get; set; } + + [Column("GenerationVersion")] + public int GenerationVersion { get; set; } = 1; + + public DietGeneratorSession? Session { get; set; } + public Recipe? Recipe { get; set; } + public ICollection PlannedIngredients { get; set; } = + new List(); +} diff --git a/MealPlan.Api/Models/DietGeneratorDraftMealIngredient.cs b/MealPlan.Api/Models/DietGeneratorDraftMealIngredient.cs new file mode 100644 index 0000000..64a83e4 --- /dev/null +++ b/MealPlan.Api/Models/DietGeneratorDraftMealIngredient.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DietGeneratorDraftMealIngredients")] +public class DietGeneratorDraftMealIngredient +{ + [Column("Id")] + public int Id { get; set; } + + [Column("DraftMealId")] + public int DraftMealId { get; set; } + + [Column("SourceIngredientId")] + public int? SourceIngredientId { get; set; } + + [Column("Name")] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [Column("AmountGrams")] + public decimal? AmountGrams { get; set; } + + [Column("Unit")] + [MaxLength(50)] + public string? Unit { get; set; } + + [Column("SortOrder")] + public int SortOrder { get; set; } + + public DietGeneratorDraftMeal? DraftMeal { get; set; } + public Ingredient? SourceIngredient { get; set; } +} diff --git a/MealPlan.Api/Models/DietGeneratorSession.cs b/MealPlan.Api/Models/DietGeneratorSession.cs new file mode 100644 index 0000000..f48e664 --- /dev/null +++ b/MealPlan.Api/Models/DietGeneratorSession.cs @@ -0,0 +1,68 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DietGeneratorSessions")] +public class DietGeneratorSession +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("Status")] + [MaxLength(32)] + public string Status { get; set; } = DietGeneratorStatuses.MacrosPending; + + [Column("ProposedCalories")] + public int? ProposedCalories { get; set; } + + [Column("ProposedProteinG")] + public decimal? ProposedProteinG { get; set; } + + [Column("ProposedFatG")] + public decimal? ProposedFatG { get; set; } + + [Column("ProposedCarbsG")] + public decimal? ProposedCarbsG { get; set; } + + [Column("MacroRationale")] + public string? MacroRationale { get; set; } + + [Column("MacrosAcceptedAt")] + public DateTime? MacrosAcceptedAt { get; set; } + + [Column("PlanStartDate")] + public DateOnly? PlanStartDate { get; set; } + + [Column("PlanDayCount")] + public int PlanDayCount { get; set; } = 7; + + [Column("CalorieToleranceKcal")] + public int CalorieToleranceKcal { get; set; } = 10; + + [Column("PreferencesJson")] + public string? PreferencesJson { get; set; } + + [Column("CommittedAt")] + public DateTime? CommittedAt { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public ICollection DraftMeals { get; set; } = new List(); +} + +public static class DietGeneratorStatuses +{ + public const string MacrosPending = "macros_pending"; + public const string MacrosAccepted = "macros_accepted"; + public const string PlanDraft = "plan_draft"; + public const string Committed = "committed"; + public const string Abandoned = "abandoned"; +} diff --git a/MealPlan.Api/Models/DietReminder.cs b/MealPlan.Api/Models/DietReminder.cs new file mode 100644 index 0000000..654b1f0 --- /dev/null +++ b/MealPlan.Api/Models/DietReminder.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DietReminders")] +public class DietReminder +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("ReminderType")] + public int ReminderType { get; set; } + + [Column("Title")] + [MaxLength(120)] + public string Title { get; set; } = string.Empty; + + [Column("Message")] + [MaxLength(500)] + public string? Message { get; set; } + + [Column("TimeOfDay")] + public TimeOnly TimeOfDay { get; set; } + + [Column("DaysOfWeekMask")] + public int DaysOfWeekMask { get; set; } = ReminderDaysMask.All; + + [Column("MealCategory")] + public int? MealCategory { get; set; } + + [Column("IsEnabled")] + public bool IsEnabled { get; set; } = true; + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public ApplicationUser? User { get; set; } +} diff --git a/MealPlan.Api/Models/DietUserProfile.cs b/MealPlan.Api/Models/DietUserProfile.cs new file mode 100644 index 0000000..eaa450f --- /dev/null +++ b/MealPlan.Api/Models/DietUserProfile.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DietUserProfiles")] +public class DietUserProfile +{ + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("HeightCm")] + public decimal HeightCm { get; set; } + + [Column("WeightKg")] + public decimal WeightKg { get; set; } + + [Column("Age")] + public int Age { get; set; } + + [Column("Sex")] + [MaxLength(16)] + public string Sex { get; set; } = "male"; + + [Column("ActivityLevel")] + [MaxLength(32)] + public string ActivityLevel { get; set; } = "moderate"; + + [Column("Goal")] + [MaxLength(32)] + public string Goal { get; set; } = "maintain"; + + [Column("AllergiesNotes")] + public string? AllergiesNotes { get; set; } + + [Column("DislikedFoods")] + public string? DislikedFoods { get; set; } + + [Column("MealsPerDayMask")] + public int MealsPerDayMask { get; set; } = 15; + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public ApplicationUser? User { get; set; } +} diff --git a/MealPlan.Api/Models/DrinkCatalogItem.cs b/MealPlan.Api/Models/DrinkCatalogItem.cs new file mode 100644 index 0000000..5189469 --- /dev/null +++ b/MealPlan.Api/Models/DrinkCatalogItem.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DrinkCatalog")] +public class DrinkCatalogItem +{ + [Column("Id")] + public int Id { get; set; } + + [Column("Code")] + [MaxLength(32)] + public string Code { get; set; } = string.Empty; + + [Column("Name")] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [Column("DefaultVolumeMl")] + public int DefaultVolumeMl { get; set; } + + [Column("CaloriesPer100Ml")] + public decimal? CaloriesPer100Ml { get; set; } + + [Column("IconEmoji")] + [MaxLength(16)] + public string? IconEmoji { get; set; } + + [Column("SortOrder")] + public int SortOrder { get; set; } + + [Column("IsActive")] + public bool IsActive { get; set; } +} diff --git a/MealPlan.Api/Models/DrinkConsumption.cs b/MealPlan.Api/Models/DrinkConsumption.cs new file mode 100644 index 0000000..03d2f73 --- /dev/null +++ b/MealPlan.Api/Models/DrinkConsumption.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("DrinkConsumptions")] +public class DrinkConsumption +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("DrinkCatalogId")] + public int? DrinkCatalogId { get; set; } + + [Column("LogDate")] + public DateOnly LogDate { get; set; } + + [Column("ConsumedAt")] + public DateTime ConsumedAt { get; set; } + + [Column("Name")] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [Column("VolumeMl")] + public int VolumeMl { get; set; } + + [Column("Calories")] + public int? Calories { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + public ApplicationUser? User { get; set; } + public DrinkCatalogItem? DrinkCatalog { get; set; } +} diff --git a/MealPlan.Api/Models/Ingredient.cs b/MealPlan.Api/Models/Ingredient.cs index 0dc6bbd..950f29a 100644 --- a/MealPlan.Api/Models/Ingredient.cs +++ b/MealPlan.Api/Models/Ingredient.cs @@ -29,5 +29,9 @@ public class Ingredient [Column("SortOrder")] public int SortOrder { get; set; } + [Column("CatalogItemId")] + public int? CatalogItemId { get; set; } + public Recipe? Recipe { get; set; } + public IngredientNutritionCatalogItem? CatalogItem { get; set; } } diff --git a/MealPlan.Api/Models/IngredientCategory.cs b/MealPlan.Api/Models/IngredientCategory.cs new file mode 100644 index 0000000..65b38ef --- /dev/null +++ b/MealPlan.Api/Models/IngredientCategory.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("IngredientCategories")] +public class IngredientCategory +{ + [Column("Id")] + public int Id { get; set; } + + [Column("Code")] + [MaxLength(32)] + public string Code { get; set; } = string.Empty; + + [Column("Name")] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [Column("SortOrder")] + public int SortOrder { get; set; } + + [Column("IsActive")] + public bool IsActive { get; set; } = true; + + public ICollection Items { get; set; } = new List(); +} diff --git a/MealPlan.Api/Models/IngredientNutritionCatalogItem.cs b/MealPlan.Api/Models/IngredientNutritionCatalogItem.cs new file mode 100644 index 0000000..e073fb3 --- /dev/null +++ b/MealPlan.Api/Models/IngredientNutritionCatalogItem.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("IngredientNutritionCatalog")] +public class IngredientNutritionCatalogItem +{ + [Column("Id")] + public int Id { get; set; } + + [Column("CategoryId")] + public int CategoryId { get; set; } + + [Column("Name")] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [Column("NamePl")] + [MaxLength(255)] + public string? NamePl { get; set; } + + [Column("CaloriesPer100G")] + public int CaloriesPer100G { get; set; } + + [Column("ProteinGPer100G")] + public decimal ProteinGPer100G { get; set; } + + [Column("FatGPer100G")] + public decimal FatGPer100G { get; set; } + + [Column("CarbsGPer100G")] + public decimal CarbsGPer100G { get; set; } + + [Column("FiberGPer100G")] + public decimal? FiberGPer100G { get; set; } + + [Column("SortOrder")] + public int SortOrder { get; set; } + + [Column("IsActive")] + public bool IsActive { get; set; } = true; + + public IngredientCategory? Category { get; set; } +} diff --git a/MealPlan.Api/Models/MealConsumption.cs b/MealPlan.Api/Models/MealConsumption.cs new file mode 100644 index 0000000..3e2f836 --- /dev/null +++ b/MealPlan.Api/Models/MealConsumption.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("MealConsumptions")] +public class MealConsumption +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("RecipeId")] + public int? RecipeId { get; set; } + + [Column("MealCategory")] + public int MealCategory { get; set; } + + [Column("LogDate")] + public DateOnly LogDate { get; set; } + + [Column("ConsumedAt")] + public DateTime ConsumedAt { get; set; } + + [Column("Name")] + [MaxLength(255)] + public string Name { get; set; } = string.Empty; + + [Column("Portions")] + public decimal Portions { get; set; } = 1m; + + [Column("Calories")] + public int? Calories { get; set; } + + [Column("ProteinG")] + public decimal? ProteinG { get; set; } + + [Column("FatG")] + public decimal? FatG { get; set; } + + [Column("CarbsG")] + public decimal? CarbsG { get; set; } + + [Column("Notes")] + public string? Notes { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + public ApplicationUser? User { get; set; } + public Recipe? Recipe { get; set; } +} diff --git a/MealPlan.Api/Models/MealPlanEntry.cs b/MealPlan.Api/Models/MealPlanEntry.cs new file mode 100644 index 0000000..9d9d95f --- /dev/null +++ b/MealPlan.Api/Models/MealPlanEntry.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("MealPlanEntries")] +public class MealPlanEntry +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("PlanDate")] + public DateOnly PlanDate { get; set; } + + [Column("MealCategory")] + public int MealCategory { get; set; } + + [Column("RecipeId")] + public int RecipeId { get; set; } + + [Column("Portions")] + public decimal Portions { get; set; } = 1m; + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public ApplicationUser? User { get; set; } + public Recipe? Recipe { get; set; } +} diff --git a/MealPlan.Api/Models/Recipe.cs b/MealPlan.Api/Models/Recipe.cs index 065b3a9..1f6074f 100644 --- a/MealPlan.Api/Models/Recipe.cs +++ b/MealPlan.Api/Models/Recipe.cs @@ -44,6 +44,16 @@ public class Recipe [Column("IsActive")] public bool IsActive { get; set; } + [Column("Source")] + [MaxLength(32)] + public string Source { get; set; } = "manual"; + + [Column("CreatedByUserId")] + public Guid? CreatedByUserId { get; set; } + + [Column("IsDraft")] + public bool IsDraft { get; set; } + public ICollection Ingredients { get; set; } = new List(); public ICollection Steps { get; set; } = new List(); } diff --git a/MealPlan.Api/Models/RecipeGeneratorSession.cs b/MealPlan.Api/Models/RecipeGeneratorSession.cs new file mode 100644 index 0000000..d9939fc --- /dev/null +++ b/MealPlan.Api/Models/RecipeGeneratorSession.cs @@ -0,0 +1,45 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("RecipeGeneratorSessions")] +public class RecipeGeneratorSession +{ + [Column("Id")] + public int Id { get; set; } + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("Status")] + [MaxLength(32)] + public string Status { get; set; } = RecipeGeneratorStatuses.Draft; + + [Column("ConstraintsJson")] + public string ConstraintsJson { get; set; } = "{}"; + + [Column("DraftJson")] + public string? DraftJson { get; set; } + + [Column("SavedRecipeId")] + public int? SavedRecipeId { get; set; } + + [Column("GenerationVersion")] + public int GenerationVersion { get; set; } = 1; + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public Recipe? SavedRecipe { get; set; } +} + +public static class RecipeGeneratorStatuses +{ + public const string Draft = "draft"; + public const string Saved = "saved"; + public const string Abandoned = "abandoned"; +} diff --git a/MealPlan.Api/Models/RefreshTokenEntity.cs b/MealPlan.Api/Models/RefreshTokenEntity.cs new file mode 100644 index 0000000..b357851 --- /dev/null +++ b/MealPlan.Api/Models/RefreshTokenEntity.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("RefreshTokens")] +public class RefreshTokenEntity +{ + [Column("Token")] + public string Token { get; set; } = string.Empty; + + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("ExpiresAtUtc")] + public DateTime ExpiresAtUtc { get; set; } + + [Column("CreatedAtUtc")] + public DateTime CreatedAtUtc { get; set; } + + [Column("IsRevoked")] + public bool IsRevoked { get; set; } + + public bool IsActive => !IsRevoked && DateTime.UtcNow < ExpiresAtUtc; + + public ApplicationUser? User { get; set; } +} diff --git a/MealPlan.Api/Models/UserDailyGoal.cs b/MealPlan.Api/Models/UserDailyGoal.cs new file mode 100644 index 0000000..9781b51 --- /dev/null +++ b/MealPlan.Api/Models/UserDailyGoal.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("UserDailyGoals")] +public class UserDailyGoal +{ + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("CalorieGoal")] + public int? CalorieGoal { get; set; } + + [Column("ProteinGoalG")] + public decimal? ProteinGoalG { get; set; } + + [Column("FatGoalG")] + public decimal? FatGoalG { get; set; } + + [Column("CarbsGoalG")] + public decimal? CarbsGoalG { get; set; } + + [Column("WaterGoalMl")] + public int? WaterGoalMl { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + [Column("UpdatedAt")] + public DateTime? UpdatedAt { get; set; } + + public ApplicationUser? User { get; set; } +} diff --git a/MealPlan.Api/Models/UserFavoriteCatalogItem.cs b/MealPlan.Api/Models/UserFavoriteCatalogItem.cs new file mode 100644 index 0000000..8859bf1 --- /dev/null +++ b/MealPlan.Api/Models/UserFavoriteCatalogItem.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("UserFavoriteCatalogItems")] +public class UserFavoriteCatalogItem +{ + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("CatalogItemId")] + public int CatalogItemId { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + public IngredientNutritionCatalogItem? CatalogItem { get; set; } +} diff --git a/MealPlan.Api/Models/UserFavoriteRecipe.cs b/MealPlan.Api/Models/UserFavoriteRecipe.cs new file mode 100644 index 0000000..b6cb0fb --- /dev/null +++ b/MealPlan.Api/Models/UserFavoriteRecipe.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace MealPlan.Api.Models; + +[Table("UserFavoriteRecipes")] +public class UserFavoriteRecipe +{ + [Column("UserId")] + public Guid UserId { get; set; } + + [Column("RecipeId")] + public int RecipeId { get; set; } + + [Column("CreatedAt")] + public DateTime CreatedAt { get; set; } + + public Recipe? Recipe { get; set; } +} diff --git a/MealPlan.Api/Program.cs b/MealPlan.Api/Program.cs index 188260e..dd8fdb0 100644 --- a/MealPlan.Api/Program.cs +++ b/MealPlan.Api/Program.cs @@ -62,12 +62,23 @@ builder.Services.AddDbContext(options => // --------------------------------------------------------------------------- // Application services // --------------------------------------------------------------------------- -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddSingleton, PasswordHasher>(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.Configure( + builder.Configuration.GetSection(MealPlan.Api.Services.AI.OpenAiOptions.SectionName)); +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // --------------------------------------------------------------------------- // Authentication / authorization (JWT bearer) @@ -163,6 +174,19 @@ builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false); var app = builder.Build(); +if (args.Contains("--fix-sequences", StringComparer.OrdinalIgnoreCase)) +{ + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var messages = await SequenceMaintenance.ResyncIdentitySequencesAsync(db); + foreach (var message in messages) + { + Console.WriteLine(message); + } + + return; +} + // --------------------------------------------------------------------------- // HTTP request pipeline // --------------------------------------------------------------------------- diff --git a/MealPlan.Api/Services/AI/IOpenAiChatService.cs b/MealPlan.Api/Services/AI/IOpenAiChatService.cs new file mode 100644 index 0000000..db5e464 --- /dev/null +++ b/MealPlan.Api/Services/AI/IOpenAiChatService.cs @@ -0,0 +1,12 @@ +namespace MealPlan.Api.Services.AI; + +public interface IOpenAiChatService +{ + bool IsConfigured { get; } + + Task CompleteJsonAsync( + string systemPrompt, + string userPrompt, + OpenAiUseCase useCase = OpenAiUseCase.Default, + CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/AI/OpenAiChatService.cs b/MealPlan.Api/Services/AI/OpenAiChatService.cs new file mode 100644 index 0000000..ae4d0c9 --- /dev/null +++ b/MealPlan.Api/Services/AI/OpenAiChatService.cs @@ -0,0 +1,80 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Options; + +namespace MealPlan.Api.Services.AI; + +public class OpenAiChatService : IOpenAiChatService +{ + private readonly HttpClient _http; + private readonly OpenAiOptions _options; + private readonly ILogger _logger; + + public OpenAiChatService(HttpClient http, IOptions options, ILogger logger) + { + _http = http; + _options = options.Value; + _logger = logger; + } + + public bool IsConfigured => _options.IsConfigured; + + public async Task CompleteJsonAsync( + string systemPrompt, + string userPrompt, + OpenAiUseCase useCase = OpenAiUseCase.Default, + CancellationToken ct = default) + { + if (!IsConfigured) + { + throw new InvalidOperationException( + "OpenAI is not configured. Set OpenAI__ApiKey in appsettings.Local.json."); + } + + var model = _options.ResolveModel(useCase); + var payload = new + { + model, + max_tokens = _options.MaxTokens, + response_format = new { type = "json_object" }, + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = userPrompt }, + }, + }; + + using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/chat/completions"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey); + request.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"); + + using var response = await _http.SendAsync(request, ct); + var body = await response.Content.ReadAsStringAsync(ct); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("OpenAI API error {Status}: {Body}", response.StatusCode, body); + throw new InvalidOperationException("OpenAI request failed. Check API key and quota."); + } + + using var doc = JsonDocument.Parse(body); + var content = doc.RootElement + .GetProperty("choices")[0] + .GetProperty("message") + .GetProperty("content") + .GetString() ?? "{}"; + + return StripMarkdownFence(content); + } + + private static string StripMarkdownFence(string content) + { + var trimmed = content.Trim(); + if (!trimmed.StartsWith("```", StringComparison.Ordinal)) return trimmed; + var firstNewline = trimmed.IndexOf('\n'); + if (firstNewline < 0) return trimmed; + var endFence = trimmed.LastIndexOf("```", StringComparison.Ordinal); + if (endFence <= firstNewline) return trimmed; + return trimmed[(firstNewline + 1)..endFence].Trim(); + } +} diff --git a/MealPlan.Api/Services/AI/OpenAiOptions.cs b/MealPlan.Api/Services/AI/OpenAiOptions.cs new file mode 100644 index 0000000..ec0ed88 --- /dev/null +++ b/MealPlan.Api/Services/AI/OpenAiOptions.cs @@ -0,0 +1,44 @@ +namespace MealPlan.Api.Services.AI; + +public enum OpenAiUseCase +{ + Default, + Macros, + Plan, + Recipe, +} + +public class OpenAiOptions +{ + public const string SectionName = "OpenAI"; + + public string ApiKey { get; set; } = string.Empty; + + /// Fallback when a task-specific model is not set. + public string Model { get; set; } = "gpt-4.1-mini"; + + /// Macro target refinement (diet generator step 2). + public string? MacrosModel { get; set; } + + /// Weekly meal plan recipe selection (diet generator step 3). + public string? PlanModel { get; set; } + + /// AI recipe draft generation and partial regen. + public string? RecipeModel { get; set; } + + public int MaxTokens { get; set; } = 4096; + + public bool IsConfigured => !string.IsNullOrWhiteSpace(ApiKey); + + public string ResolveModel(OpenAiUseCase useCase) => + useCase switch + { + OpenAiUseCase.Macros => FirstNonEmpty(MacrosModel, Model), + OpenAiUseCase.Plan => FirstNonEmpty(PlanModel, Model), + OpenAiUseCase.Recipe => FirstNonEmpty(RecipeModel, Model), + _ => Model, + }; + + private static string FirstNonEmpty(string? preferred, string fallback) => + string.IsNullOrWhiteSpace(preferred) ? fallback : preferred; +} diff --git a/MealPlan.Api/Services/DbRefreshTokenStore.cs b/MealPlan.Api/Services/DbRefreshTokenStore.cs new file mode 100644 index 0000000..beaea41 --- /dev/null +++ b/MealPlan.Api/Services/DbRefreshTokenStore.cs @@ -0,0 +1,68 @@ +using MealPlan.Api.Data; +using MealPlan.Api.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace MealPlan.Api.Services; + +/// Persists refresh tokens in PostgreSQL (survives API restarts). +public class DbRefreshTokenStore : IRefreshTokenStore +{ + private readonly IServiceScopeFactory _scopeFactory; + + public DbRefreshTokenStore(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; + + public void Add(RefreshToken token) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.RefreshTokens.Add(new RefreshTokenEntity + { + Token = token.Token, + UserId = Guid.Parse(token.UserId), + ExpiresAtUtc = token.ExpiresAtUtc, + CreatedAtUtc = token.CreatedAtUtc, + IsRevoked = false, + }); + db.SaveChanges(); + } + + public RefreshToken? Get(string token) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var entity = db.RefreshTokens.AsNoTracking().FirstOrDefault(t => t.Token == token); + return entity is null ? null : Map(entity); + } + + public RefreshToken? Revoke(string token) + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var entity = db.RefreshTokens.FirstOrDefault(t => t.Token == token); + if (entity is null) return null; + var wasActive = entity.IsActive; + entity.IsRevoked = true; + db.SaveChanges(); + return wasActive ? Map(entity) : null; + } + + public void RemoveExpired() + { + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var cutoff = DateTime.UtcNow; + db.RefreshTokens + .Where(t => t.IsRevoked || t.ExpiresAtUtc <= cutoff) + .ExecuteDelete(); + } + + private static RefreshToken Map(RefreshTokenEntity entity) => new() + { + Token = entity.Token, + UserId = entity.UserId.ToString(), + ExpiresAtUtc = entity.ExpiresAtUtc, + CreatedAtUtc = entity.CreatedAtUtc, + IsRevoked = entity.IsRevoked, + }; +} diff --git a/MealPlan.Api/Services/DietTrackingService.cs b/MealPlan.Api/Services/DietTrackingService.cs new file mode 100644 index 0000000..fc3b495 --- /dev/null +++ b/MealPlan.Api/Services/DietTrackingService.cs @@ -0,0 +1,850 @@ +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.Diet; +using MealPlan.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services; + +public class DietTrackingService : IDietTrackingService +{ + private readonly AppDbContext _db; + private readonly IRecipeService _recipeService; + + public DietTrackingService(AppDbContext db, IRecipeService recipeService) + { + _db = db; + _recipeService = recipeService; + } + + public async Task GetGoalsAsync(Guid userId, CancellationToken ct = default) + { + var goals = await _db.UserDailyGoals.AsNoTracking() + .FirstOrDefaultAsync(g => g.UserId == userId, ct); + return goals is null ? null : MapGoals(goals); + } + + public async Task UpsertGoalsAsync(Guid userId, UserDailyGoalsDto dto, CancellationToken ct = default) + { + var existing = await _db.UserDailyGoals.FirstOrDefaultAsync(g => g.UserId == userId, ct); + if (existing is null) + { + existing = new UserDailyGoal + { + UserId = userId, + CreatedAt = DateTime.UtcNow, + }; + _db.UserDailyGoals.Add(existing); + } + + existing.CalorieGoal = dto.CalorieGoal; + existing.ProteinGoalG = dto.ProteinGoalG; + existing.FatGoalG = dto.FatGoalG; + existing.CarbsGoalG = dto.CarbsGoalG; + existing.WaterGoalMl = dto.WaterGoalMl; + existing.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(ct); + return MapGoals(existing); + } + + public async Task GetDailySummaryAsync(Guid userId, DateOnly date, CancellationToken ct = default) + { + var goals = await GetGoalsAsync(userId, ct); + var meals = await GetMealsAsync(userId, date, ct); + var drinks = await GetDrinksAsync(userId, date, ct); + + var totalCalories = meals.Sum(m => m.Calories ?? 0); + var totalProtein = meals.Sum(m => m.ProteinG ?? 0); + var totalFat = meals.Sum(m => m.FatG ?? 0); + var totalCarbs = meals.Sum(m => m.CarbsG ?? 0); + var totalWater = drinks.Sum(d => d.VolumeMl); + + return new DailySummaryDto + { + Date = date, + Goals = goals, + Calories = BuildIntMacro(goals?.CalorieGoal, totalCalories), + ProteinG = BuildDecimalMacro(goals?.ProteinGoalG, totalProtein), + FatG = BuildDecimalMacro(goals?.FatGoalG, totalFat), + CarbsG = BuildDecimalMacro(goals?.CarbsGoalG, totalCarbs), + WaterMl = BuildIntMacro(goals?.WaterGoalMl, totalWater), + Meals = meals, + Drinks = drinks, + }; + } + + public async Task> GetMealsAsync(Guid userId, DateOnly date, CancellationToken ct = default) + { + var rows = await _db.MealConsumptions.AsNoTracking() + .Where(m => m.UserId == userId && m.LogDate == date) + .OrderByDescending(m => m.ConsumedAt) + .ToListAsync(ct); + return rows.Select(MapMeal).ToList(); + } + + public async Task LogMealAsync(Guid userId, LogMealDto dto, CancellationToken ct = default) + { + var consumedAt = dto.ConsumedAt ?? DateTime.UtcNow; + var logDate = dto.LogDate ?? DateOnly.FromDateTime(consumedAt); + + string name; + int? calories; + decimal? protein; + decimal? fat; + decimal? carbs; + int? recipeId = dto.RecipeId; + + if (dto.RecipeId.HasValue) + { + var recipe = await _recipeService.GetRecipeByIdAsync(dto.RecipeId.Value, ct); + if (recipe is null) + { + return null; + } + + name = recipe.Name; + calories = ScaleInt(recipe.Calories, dto.Portions); + protein = ScaleDecimal(recipe.Protein, dto.Portions); + fat = ScaleDecimal(recipe.Fat, dto.Portions); + carbs = ScaleDecimal(recipe.Carbs, dto.Portions); + + if (dto.Calories.HasValue) calories = dto.Calories; + if (dto.ProteinG.HasValue) protein = dto.ProteinG; + if (dto.FatG.HasValue) fat = dto.FatG; + if (dto.CarbsG.HasValue) carbs = dto.CarbsG; + } + else if (dto.CatalogItemId.HasValue && dto.Grams is > 0) + { + var catalog = await _db.IngredientNutritionCatalog.AsNoTracking() + .FirstOrDefaultAsync(i => i.Id == dto.CatalogItemId && i.IsActive, ct); + if (catalog is null) return null; + + name = string.IsNullOrWhiteSpace(dto.Name) ? catalog.Name : dto.Name.Trim(); + var scale = dto.Grams.Value / 100m; + calories = (int)Math.Round(catalog.CaloriesPer100G * scale); + protein = Math.Round(catalog.ProteinGPer100G * scale, 2); + fat = Math.Round(catalog.FatGPer100G * scale, 2); + carbs = Math.Round(catalog.CarbsGPer100G * scale, 2); + recipeId = null; + } + else if (!string.IsNullOrWhiteSpace(dto.Name)) + { + name = dto.Name.Trim(); + calories = dto.Calories; + protein = dto.ProteinG; + fat = dto.FatG; + carbs = dto.CarbsG; + recipeId = null; + } + else + { + return null; + } + + var entity = new MealConsumption + { + UserId = userId, + RecipeId = recipeId, + MealCategory = dto.MealCategory, + LogDate = logDate, + ConsumedAt = consumedAt, + Name = name, + Portions = dto.Portions, + Calories = calories, + ProteinG = protein, + FatG = fat, + CarbsG = carbs, + Notes = dto.Notes, + CreatedAt = DateTime.UtcNow, + }; + + _db.MealConsumptions.Add(entity); + await _db.SaveChangesAsync(ct); + return MapMeal(entity); + } + + public async Task PreviewMealAsync( + Guid userId, + DateOnly date, + LogMealDto dto, + CancellationToken ct = default) + { + var summary = await GetDailySummaryAsync(userId, date, ct); + var snapshot = await ResolveMealSnapshotAsync(dto, ct); + if (snapshot is null) return null; + + return new MealPreviewDto + { + Meal = snapshot, + RemainingAfter = BuildRemainingAfter(summary, snapshot), + }; + } + + public async Task> GetMealSuggestionsAsync( + Guid userId, + DateOnly date, + int limit = 5, + CancellationToken ct = default) + { + limit = Math.Clamp(limit, 1, 10); + var summary = await GetDailySummaryAsync(userId, date, ct); + var items = await _db.IngredientNutritionCatalog.AsNoTracking() + .Include(i => i.Category) + .Where(i => i.IsActive && i.Category!.IsActive) + .ToListAsync(ct); + + if (items.Count == 0) return Array.Empty(); + + var reasonKey = PickPrimaryDeficit(summary); + const int defaultGrams = 150; + + var scored = items.Select(item => + { + var scale = defaultGrams / 100m; + var calories = (int)Math.Round(item.CaloriesPer100G * scale); + var protein = Math.Round(item.ProteinGPer100G * scale, 2); + var fat = Math.Round(item.FatGPer100G * scale, 2); + var carbs = Math.Round(item.CarbsGPer100G * scale, 2); + var score = ScoreSuggestion(summary, reasonKey, calories, protein, fat, carbs); + + return ( + score, + Suggestion: new DietSuggestionDto + { + CatalogItemId = item.Id, + Name = item.Name, + NamePl = item.NamePl, + CategoryCode = item.Category?.Code ?? string.Empty, + SuggestedGrams = defaultGrams, + Calories = calories, + ProteinG = protein, + FatG = fat, + CarbsG = carbs, + ReasonKey = reasonKey, + }); + }) + .OrderByDescending(x => x.score) + .ThenBy(x => x.Suggestion.Name) + .Take(limit) + .Select(x => x.Suggestion) + .ToList(); + + return scored; + } + + private async Task ResolveMealSnapshotAsync(LogMealDto dto, CancellationToken ct) + { + if (dto.RecipeId.HasValue) + { + var recipe = await _recipeService.GetRecipeByIdAsync(dto.RecipeId.Value, ct); + if (recipe is null) return null; + + return new MealMacroSnapshotDto + { + Name = recipe.Name, + Portions = dto.Portions, + Calories = ScaleInt(recipe.Calories, dto.Portions), + ProteinG = ScaleDecimal(recipe.Protein, dto.Portions), + FatG = ScaleDecimal(recipe.Fat, dto.Portions), + CarbsG = ScaleDecimal(recipe.Carbs, dto.Portions), + }; + } + + if (dto.CatalogItemId.HasValue && dto.Grams is > 0) + { + var catalog = await _db.IngredientNutritionCatalog.AsNoTracking() + .FirstOrDefaultAsync(i => i.Id == dto.CatalogItemId && i.IsActive, ct); + if (catalog is null) return null; + + var scale = dto.Grams.Value / 100m; + return new MealMacroSnapshotDto + { + Name = string.IsNullOrWhiteSpace(dto.Name) ? catalog.Name : dto.Name.Trim(), + Portions = 1, + Calories = (int)Math.Round(catalog.CaloriesPer100G * scale), + ProteinG = Math.Round(catalog.ProteinGPer100G * scale, 2), + FatG = Math.Round(catalog.FatGPer100G * scale, 2), + CarbsG = Math.Round(catalog.CarbsGPer100G * scale, 2), + }; + } + + if (!string.IsNullOrWhiteSpace(dto.Name)) + { + return new MealMacroSnapshotDto + { + Name = dto.Name.Trim(), + Portions = dto.Portions, + Calories = dto.Calories, + ProteinG = dto.ProteinG, + FatG = dto.FatG, + CarbsG = dto.CarbsG, + }; + } + + return null; + } + + private static RemainingAfterMealDto BuildRemainingAfter(DailySummaryDto summary, MealMacroSnapshotDto meal) + { + int? RemainingInt(int? goal, int consumed, int? mealValue) => + goal.HasValue ? Math.Max(goal.Value - consumed - (mealValue ?? 0), 0) : null; + + decimal? RemainingDec(decimal? goal, decimal consumed, decimal? mealValue) => + goal.HasValue ? Math.Max(goal.Value - consumed - (mealValue ?? 0), 0) : null; + + return new RemainingAfterMealDto + { + Calories = RemainingInt(summary.Calories.Goal, summary.Calories.Consumed, meal.Calories), + ProteinG = RemainingDec(summary.ProteinG.Goal, summary.ProteinG.Consumed, meal.ProteinG), + FatG = RemainingDec(summary.FatG.Goal, summary.FatG.Consumed, meal.FatG), + CarbsG = RemainingDec(summary.CarbsG.Goal, summary.CarbsG.Consumed, meal.CarbsG), + }; + } + + private static string PickPrimaryDeficit(DailySummaryDto summary) + { + var deficits = new List<(string Key, decimal Weight)>(); + + if (summary.ProteinG.Goal is > 0 && summary.ProteinG.Remaining is > 0) + { + deficits.Add(("highProtein", summary.ProteinG.Remaining.Value / summary.ProteinG.Goal.Value)); + } + + if (summary.CarbsG.Goal is > 0 && summary.CarbsG.Remaining is > 0) + { + deficits.Add(("needCarbs", summary.CarbsG.Remaining.Value / summary.CarbsG.Goal.Value)); + } + + if (summary.FatG.Goal is > 0 && summary.FatG.Remaining is > 0) + { + deficits.Add(("needFat", summary.FatG.Remaining.Value / summary.FatG.Goal.Value)); + } + + if (summary.Calories.Goal is > 0 && summary.Calories.Remaining is > 0) + { + deficits.Add(("needCalories", (decimal)summary.Calories.Remaining.Value / summary.Calories.Goal.Value)); + } + + return deficits.OrderByDescending(d => d.Weight).FirstOrDefault().Key ?? "balanced"; + } + + private static decimal ScoreSuggestion( + DailySummaryDto summary, + string reasonKey, + int calories, + decimal protein, + decimal fat, + decimal carbs) + { + if (calories <= 0) return 0; + + return reasonKey switch + { + "highProtein" => protein * 2m + protein / calories, + "needCarbs" => carbs * 2m + carbs / calories, + "needFat" => fat * 2m + fat / calories, + "needCalories" => calories, + _ => protein + carbs + (1000m / Math.Max(calories, 1)), + }; + } + + public async Task DeleteMealAsync(Guid userId, int id, CancellationToken ct = default) + { + var entity = await _db.MealConsumptions.FirstOrDefaultAsync(m => m.Id == id && m.UserId == userId, ct); + if (entity is null) return false; + _db.MealConsumptions.Remove(entity); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task> GetDrinkCatalogAsync(CancellationToken ct = default) + { + return await _db.DrinkCatalog.AsNoTracking() + .Where(d => d.IsActive) + .OrderBy(d => d.SortOrder) + .Select(d => new DrinkCatalogItemDto + { + Id = d.Id, + Code = d.Code, + Name = d.Name, + DefaultVolumeMl = d.DefaultVolumeMl, + CaloriesPer100Ml = d.CaloriesPer100Ml, + IconEmoji = d.IconEmoji, + }) + .ToListAsync(ct); + } + + public async Task> GetDrinksAsync(Guid userId, DateOnly date, CancellationToken ct = default) + { + var rows = await _db.DrinkConsumptions.AsNoTracking() + .Where(d => d.UserId == userId && d.LogDate == date) + .OrderByDescending(d => d.ConsumedAt) + .ToListAsync(ct); + return rows.Select(MapDrink).ToList(); + } + + public async Task LogDrinkAsync(Guid userId, LogDrinkDto dto, CancellationToken ct = default) + { + var consumedAt = dto.ConsumedAt ?? DateTime.UtcNow; + var logDate = dto.LogDate ?? DateOnly.FromDateTime(consumedAt); + + string name; + int? catalogId = dto.DrinkCatalogId; + int? calories = dto.Calories; + + if (dto.DrinkCatalogId.HasValue) + { + var catalog = await _db.DrinkCatalog.AsNoTracking() + .FirstOrDefaultAsync(d => d.Id == dto.DrinkCatalogId && d.IsActive, ct); + if (catalog is null) return null; + + name = dto.Name?.Trim() ?? catalog.Name; + if (!calories.HasValue && catalog.CaloriesPer100Ml.HasValue) + { + calories = (int)Math.Round(catalog.CaloriesPer100Ml.Value * dto.VolumeMl / 100m); + } + } + else + { + name = dto.Name!.Trim(); + catalogId = null; + } + + var entity = new DrinkConsumption + { + UserId = userId, + DrinkCatalogId = catalogId, + LogDate = logDate, + ConsumedAt = consumedAt, + Name = name, + VolumeMl = dto.VolumeMl, + Calories = calories, + CreatedAt = DateTime.UtcNow, + }; + + _db.DrinkConsumptions.Add(entity); + await _db.SaveChangesAsync(ct); + return MapDrink(entity); + } + + public async Task DeleteDrinkAsync(Guid userId, int id, CancellationToken ct = default) + { + var entity = await _db.DrinkConsumptions.FirstOrDefaultAsync(d => d.Id == id && d.UserId == userId, ct); + if (entity is null) return false; + _db.DrinkConsumptions.Remove(entity); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task> GetRemindersAsync(Guid userId, CancellationToken ct = default) + { + var rows = await _db.DietReminders.AsNoTracking() + .Where(r => r.UserId == userId) + .OrderBy(r => r.TimeOfDay) + .ToListAsync(ct); + return rows.Select(MapReminder).ToList(); + } + + public async Task CreateReminderAsync(Guid userId, CreateDietReminderDto dto, CancellationToken ct = default) + { + var entity = new DietReminder + { + UserId = userId, + ReminderType = dto.ReminderType, + Title = dto.Title.Trim(), + Message = dto.Message?.Trim(), + TimeOfDay = dto.TimeOfDay, + DaysOfWeekMask = dto.DaysOfWeekMask, + MealCategory = dto.MealCategory, + IsEnabled = dto.IsEnabled, + CreatedAt = DateTime.UtcNow, + }; + + _db.DietReminders.Add(entity); + await _db.SaveChangesAsync(ct); + return MapReminder(entity); + } + + public async Task UpdateReminderAsync(Guid userId, int id, UpdateDietReminderDto dto, CancellationToken ct = default) + { + var entity = await _db.DietReminders.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct); + if (entity is null) return null; + + entity.ReminderType = dto.ReminderType; + entity.Title = dto.Title.Trim(); + entity.Message = dto.Message?.Trim(); + entity.TimeOfDay = dto.TimeOfDay; + entity.DaysOfWeekMask = dto.DaysOfWeekMask; + entity.MealCategory = dto.MealCategory; + entity.IsEnabled = dto.IsEnabled; + entity.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(ct); + return MapReminder(entity); + } + + public async Task DeleteReminderAsync(Guid userId, int id, CancellationToken ct = default) + { + var entity = await _db.DietReminders.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct); + if (entity is null) return false; + _db.DietReminders.Remove(entity); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task> GetDueRemindersAsync( + Guid userId, + DateOnly? date, + TimeOnly? time, + CancellationToken ct = default) + { + var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow); + var targetTime = time ?? TimeOnly.FromDateTime(DateTime.UtcNow); + var dayOfWeek = targetDate.ToDateTime(TimeOnly.MinValue).DayOfWeek; + + var reminders = await _db.DietReminders.AsNoTracking() + .Where(r => r.UserId == userId && r.IsEnabled) + .ToListAsync(ct); + + return reminders + .Where(r => ReminderDaysMask.IncludesDay(r.DaysOfWeekMask, dayOfWeek)) + .Where(r => r.TimeOfDay.Hour == targetTime.Hour && r.TimeOfDay.Minute == targetTime.Minute) + .Select(r => new DueReminderDto + { + Id = r.Id, + ReminderType = r.ReminderType, + Title = r.Title, + Message = r.Message, + TimeOfDay = r.TimeOfDay, + MealCategory = r.MealCategory, + }) + .ToList(); + } + + public async Task> GetHistoryAsync( + Guid userId, + DateOnly from, + DateOnly to, + CancellationToken ct = default) + { + var meals = await _db.MealConsumptions.AsNoTracking() + .Where(m => m.UserId == userId && m.LogDate >= from && m.LogDate <= to) + .GroupBy(m => m.LogDate) + .Select(g => new + { + Date = g.Key, + Calories = g.Sum(m => m.Calories ?? 0), + ProteinG = g.Sum(m => m.ProteinG ?? 0), + FatG = g.Sum(m => m.FatG ?? 0), + CarbsG = g.Sum(m => m.CarbsG ?? 0), + MealCount = g.Count(), + }) + .ToListAsync(ct); + + var drinks = await _db.DrinkConsumptions.AsNoTracking() + .Where(d => d.UserId == userId && d.LogDate >= from && d.LogDate <= to) + .GroupBy(d => d.LogDate) + .Select(g => new { Date = g.Key, WaterMl = g.Sum(d => d.VolumeMl) }) + .ToListAsync(ct); + + var drinkMap = drinks.ToDictionary(d => d.Date, d => d.WaterMl); + var days = new List(); + for (var d = from; d <= to; d = d.AddDays(1)) + { + var meal = meals.FirstOrDefault(m => m.Date == d); + days.Add(new HistoryDayDto + { + Date = d, + Calories = meal?.Calories ?? 0, + ProteinG = meal?.ProteinG ?? 0, + FatG = meal?.FatG ?? 0, + CarbsG = meal?.CarbsG ?? 0, + WaterMl = drinkMap.GetValueOrDefault(d, 0), + MealCount = meal?.MealCount ?? 0, + }); + } + + return days; + } + + public async Task> GetRecentMealsAsync( + Guid userId, + int limit = 10, + CancellationToken ct = default) + { + var rows = await _db.MealConsumptions.AsNoTracking() + .Where(m => m.UserId == userId) + .OrderByDescending(m => m.ConsumedAt) + .Take(Math.Clamp(limit * 3, 10, 100)) + .ToListAsync(ct); + + var seen = new HashSet(); + var result = new List(); + foreach (var m in rows) + { + var key = m.RecipeId.HasValue + ? $"r:{m.RecipeId}:{m.MealCategory}" + : $"n:{m.Name.ToLowerInvariant()}:{m.MealCategory}"; + if (!seen.Add(key)) continue; + + result.Add(new RecentMealDto + { + RecipeId = m.RecipeId, + Name = m.Name, + MealCategory = m.MealCategory, + Portions = m.Portions, + Calories = m.Calories, + ProteinG = m.ProteinG, + FatG = m.FatG, + CarbsG = m.CarbsG, + LastLoggedAt = m.ConsumedAt, + }); + if (result.Count >= limit) break; + } + + return result; + } + + public async Task CopyYesterdayMealsAsync( + Guid userId, + DateOnly targetDate, + CancellationToken ct = default) + { + var sourceDate = targetDate.AddDays(-1); + var sourceMeals = await _db.MealConsumptions + .Where(m => m.UserId == userId && m.LogDate == sourceDate) + .ToListAsync(ct); + + if (sourceMeals.Count == 0) + { + return new CopyMealsResultDto + { + SourceDate = sourceDate, + TargetDate = targetDate, + CopiedCount = 0, + }; + } + + var now = DateTime.UtcNow; + foreach (var src in sourceMeals) + { + _db.MealConsumptions.Add(new MealConsumption + { + UserId = userId, + RecipeId = src.RecipeId, + MealCategory = src.MealCategory, + LogDate = targetDate, + ConsumedAt = now, + Name = src.Name, + Portions = src.Portions, + Calories = src.Calories, + ProteinG = src.ProteinG, + FatG = src.FatG, + CarbsG = src.CarbsG, + Notes = src.Notes, + CreatedAt = now, + }); + } + + await _db.SaveChangesAsync(ct); + return new CopyMealsResultDto + { + SourceDate = sourceDate, + TargetDate = targetDate, + CopiedCount = sourceMeals.Count, + }; + } + + public async Task> GetFavoriteRecipesAsync( + Guid userId, + CancellationToken ct = default) + { + return await _db.UserFavoriteRecipes.AsNoTracking() + .Include(f => f.Recipe) + .Where(f => f.UserId == userId && f.Recipe!.IsActive) + .OrderBy(f => f.Recipe!.Name) + .Select(f => new FavoriteRecipeDto + { + RecipeId = f.RecipeId, + Name = f.Recipe!.Name, + MealCategory = f.Recipe!.MealCategory, + Calories = f.Recipe!.Calories, + }) + .ToListAsync(ct); + } + + public async Task AddFavoriteRecipeAsync( + Guid userId, + int recipeId, + CancellationToken ct = default) + { + var recipe = await _db.Recipes.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == recipeId && r.IsActive, ct); + if (recipe is null) return null; + + var exists = await _db.UserFavoriteRecipes + .AnyAsync(f => f.UserId == userId && f.RecipeId == recipeId, ct); + if (!exists) + { + _db.UserFavoriteRecipes.Add(new UserFavoriteRecipe + { + UserId = userId, + RecipeId = recipeId, + CreatedAt = DateTime.UtcNow, + }); + await _db.SaveChangesAsync(ct); + } + + return new FavoriteRecipeDto + { + RecipeId = recipe.Id, + Name = recipe.Name, + MealCategory = recipe.MealCategory, + Calories = recipe.Calories, + }; + } + + public async Task RemoveFavoriteRecipeAsync(Guid userId, int recipeId, CancellationToken ct = default) + { + var fav = await _db.UserFavoriteRecipes + .FirstOrDefaultAsync(f => f.UserId == userId && f.RecipeId == recipeId, ct); + if (fav is null) return false; + _db.UserFavoriteRecipes.Remove(fav); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task> GetFavoriteCatalogItemsAsync( + Guid userId, + CancellationToken ct = default) + { + return await _db.UserFavoriteCatalogItems.AsNoTracking() + .Include(f => f.CatalogItem) + .ThenInclude(c => c!.Category) + .Where(f => f.UserId == userId && f.CatalogItem!.IsActive) + .OrderBy(f => f.CatalogItem!.Name) + .Select(f => new FavoriteCatalogItemDto + { + CatalogItemId = f.CatalogItemId, + Name = f.CatalogItem!.Name, + NamePl = f.CatalogItem!.NamePl, + CategoryCode = f.CatalogItem!.Category!.Code, + }) + .ToListAsync(ct); + } + + public async Task AddFavoriteCatalogItemAsync( + Guid userId, + int catalogItemId, + CancellationToken ct = default) + { + var item = await _db.IngredientNutritionCatalog.AsNoTracking() + .Include(i => i.Category) + .FirstOrDefaultAsync(i => i.Id == catalogItemId && i.IsActive, ct); + if (item is null) return null; + + var exists = await _db.UserFavoriteCatalogItems + .AnyAsync(f => f.UserId == userId && f.CatalogItemId == catalogItemId, ct); + if (!exists) + { + _db.UserFavoriteCatalogItems.Add(new UserFavoriteCatalogItem + { + UserId = userId, + CatalogItemId = catalogItemId, + CreatedAt = DateTime.UtcNow, + }); + await _db.SaveChangesAsync(ct); + } + + return new FavoriteCatalogItemDto + { + CatalogItemId = item.Id, + Name = item.Name, + NamePl = item.NamePl, + CategoryCode = item.Category?.Code ?? string.Empty, + }; + } + + public async Task RemoveFavoriteCatalogItemAsync( + Guid userId, + int catalogItemId, + CancellationToken ct = default) + { + var fav = await _db.UserFavoriteCatalogItems + .FirstOrDefaultAsync(f => f.UserId == userId && f.CatalogItemId == catalogItemId, ct); + if (fav is null) return false; + _db.UserFavoriteCatalogItems.Remove(fav); + await _db.SaveChangesAsync(ct); + return true; + } + + private static UserDailyGoalsDto MapGoals(UserDailyGoal g) => new() + { + CalorieGoal = g.CalorieGoal, + ProteinGoalG = g.ProteinGoalG, + FatGoalG = g.FatGoalG, + CarbsGoalG = g.CarbsGoalG, + WaterGoalMl = g.WaterGoalMl, + }; + + private static MealConsumptionDto MapMeal(MealConsumption m) => new() + { + Id = m.Id, + RecipeId = m.RecipeId, + MealCategory = m.MealCategory, + LogDate = m.LogDate, + ConsumedAt = m.ConsumedAt, + Name = m.Name, + Portions = m.Portions, + Calories = m.Calories, + ProteinG = m.ProteinG, + FatG = m.FatG, + CarbsG = m.CarbsG, + Notes = m.Notes, + }; + + private static DrinkConsumptionDto MapDrink(DrinkConsumption d) => new() + { + Id = d.Id, + DrinkCatalogId = d.DrinkCatalogId, + LogDate = d.LogDate, + ConsumedAt = d.ConsumedAt, + Name = d.Name, + VolumeMl = d.VolumeMl, + Calories = d.Calories, + }; + + private static DietReminderDto MapReminder(DietReminder r) => new() + { + Id = r.Id, + ReminderType = r.ReminderType, + Title = r.Title, + Message = r.Message, + TimeOfDay = r.TimeOfDay, + DaysOfWeekMask = r.DaysOfWeekMask, + MealCategory = r.MealCategory, + IsEnabled = r.IsEnabled, + }; + + private static MacroRemainingDto BuildIntMacro(int? goal, int consumed) => new() + { + Goal = goal, + Consumed = consumed, + Remaining = goal.HasValue ? Math.Max(goal.Value - consumed, 0) : null, + ProgressPercent = goal is > 0 ? Math.Round(consumed * 100m / goal.Value, 1) : null, + }; + + private static DecimalMacroRemainingDto BuildDecimalMacro(decimal? goal, decimal consumed) => new() + { + Goal = goal, + Consumed = consumed, + Remaining = goal.HasValue ? Math.Max(goal.Value - consumed, 0) : null, + ProgressPercent = goal is > 0 ? Math.Round(consumed * 100m / goal.Value, 1) : null, + }; + + private static int? ScaleInt(int? value, decimal portions) => + value.HasValue ? (int)Math.Round(value.Value * portions) : null; + + private static decimal? ScaleDecimal(decimal? value, decimal portions) => + value.HasValue ? Math.Round(value.Value * portions, 2) : null; +} diff --git a/MealPlan.Api/Services/Generators/AiRecipeJsonParser.cs b/MealPlan.Api/Services/Generators/AiRecipeJsonParser.cs new file mode 100644 index 0000000..1cceb3c --- /dev/null +++ b/MealPlan.Api/Services/Generators/AiRecipeJsonParser.cs @@ -0,0 +1,39 @@ +using System.Text.Json; + +namespace MealPlan.Api.Services.Generators; + +internal static class AiRecipeJsonParser +{ + public static int GetInt32(JsonElement element, int fallback = 0) + { + return element.ValueKind switch + { + JsonValueKind.Number when element.TryGetInt32(out var value) => value, + JsonValueKind.Number => (int)element.GetDecimal(), + JsonValueKind.String when int.TryParse(element.GetString(), out var parsed) => parsed, + JsonValueKind.True => 1, + JsonValueKind.False => 0, + _ => fallback, + }; + } + + public static int? GetNullableInt32(JsonElement element) + { + if (element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) return null; + return GetInt32(element); + } + + public static decimal? GetNullableDecimal(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.Null or JsonValueKind.Undefined => null, + JsonValueKind.Number => element.GetDecimal(), + JsonValueKind.String when decimal.TryParse(element.GetString(), out var parsed) => parsed, + _ => null, + }; + } + + public static string? GetString(JsonElement element) => + element.ValueKind == JsonValueKind.String ? element.GetString() : element.ToString(); +} diff --git a/MealPlan.Api/Services/Generators/CatalogMatchingService.cs b/MealPlan.Api/Services/Generators/CatalogMatchingService.cs new file mode 100644 index 0000000..f6c5aa0 --- /dev/null +++ b/MealPlan.Api/Services/Generators/CatalogMatchingService.cs @@ -0,0 +1,60 @@ +using MealPlan.Api.Data; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services.Generators; + +public interface ICatalogMatchingService +{ + Task ResolveCatalogItemIdAsync(string nameOrCatalogName, CancellationToken ct = default); +} + +public class CatalogMatchingService : ICatalogMatchingService +{ + private readonly AppDbContext _db; + private List? _cache; + + public CatalogMatchingService(AppDbContext db) => _db = db; + + public async Task ResolveCatalogItemIdAsync(string nameOrCatalogName, CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(nameOrCatalogName)) return null; + + var cache = await GetCacheAsync(ct); + var key = Normalize(nameOrCatalogName); + + var exact = cache.FirstOrDefault(c => + c.NameKey == key || c.NamePlKey == key || c.CatalogNameKey == key); + if (exact is not null) return exact.Id; + + return cache + .Where(c => key.Contains(c.NameKey, StringComparison.Ordinal) || + c.NameKey.Contains(key, StringComparison.Ordinal) || + (!string.IsNullOrEmpty(c.NamePlKey) && + (key.Contains(c.NamePlKey, StringComparison.Ordinal) || + c.NamePlKey.Contains(key, StringComparison.Ordinal)))) + .OrderBy(c => Math.Abs(c.NameKey.Length - key.Length)) + .FirstOrDefault() + ?.Id; + } + + private async Task> GetCacheAsync(CancellationToken ct) + { + if (_cache is not null) return _cache; + + _cache = await _db.IngredientNutritionCatalog.AsNoTracking() + .Where(i => i.IsActive) + .Select(i => new CatalogEntry(i.Id, i.Name, i.NamePl)) + .ToListAsync(ct); + + return _cache; + } + + private static string Normalize(string value) => value.Trim().ToLowerInvariant(); + + private sealed record CatalogEntry(int Id, string Name, string? NamePl) + { + public string NameKey { get; } = Name.Trim().ToLowerInvariant(); + public string NamePlKey { get; } = NamePl?.Trim().ToLowerInvariant() ?? string.Empty; + public string CatalogNameKey { get; } = Name.Trim().ToLowerInvariant(); + } +} diff --git a/MealPlan.Api/Services/Generators/DietGeneratorService.cs b/MealPlan.Api/Services/Generators/DietGeneratorService.cs new file mode 100644 index 0000000..24ab9bf --- /dev/null +++ b/MealPlan.Api/Services/Generators/DietGeneratorService.cs @@ -0,0 +1,780 @@ +using System.Text.Json; +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.Diet; +using MealPlan.Api.DTOs.Generators; +using MealPlan.Api.Models; +using MealPlan.Api.Services.AI; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services.Generators; + +public interface IDietGeneratorService +{ + Task GetProfileAsync(Guid userId, CancellationToken ct = default); + Task UpsertProfileAsync(Guid userId, DietUserProfileDto dto, CancellationToken ct = default); + Task CreateSessionAsync(Guid userId, CreateDietGeneratorSessionDto dto, CancellationToken ct = default); + Task GetSessionAsync(Guid userId, int sessionId, CancellationToken ct = default); + Task ProposeMacrosAsync(Guid userId, int sessionId, CancellationToken ct = default); + Task AcceptMacrosAsync(Guid userId, int sessionId, AcceptMacrosDto dto, CancellationToken ct = default); + Task GeneratePlanAsync(Guid userId, int sessionId, CancellationToken ct = default); + Task RegenerateMealAsync(Guid userId, int sessionId, RegenerateDietMealDto dto, CancellationToken ct = default); + Task UpdateDraftMealAsync(Guid userId, int sessionId, int mealId, UpdateDraftMealDto dto, CancellationToken ct = default); + Task CommitAsync(Guid userId, int sessionId, CancellationToken ct = default); +} + +public class DietGeneratorService : IDietGeneratorService +{ + private readonly AppDbContext _db; + private readonly IOpenAiChatService _openAi; + + public DietGeneratorService(AppDbContext db, IOpenAiChatService openAi) + { + _db = db; + _openAi = openAi; + } + + public async Task GetProfileAsync(Guid userId, CancellationToken ct = default) + { + var profile = await _db.DietUserProfiles.AsNoTracking().FirstOrDefaultAsync(p => p.UserId == userId, ct); + return profile is null ? null : MapProfile(profile); + } + + public async Task UpsertProfileAsync(Guid userId, DietUserProfileDto dto, CancellationToken ct = default) + { + var profile = await _db.DietUserProfiles.FirstOrDefaultAsync(p => p.UserId == userId, ct); + if (profile is null) + { + profile = new DietUserProfile { UserId = userId, CreatedAt = DateTime.UtcNow }; + _db.DietUserProfiles.Add(profile); + } + + profile.HeightCm = dto.HeightCm; + profile.WeightKg = dto.WeightKg; + profile.Age = dto.Age; + profile.Sex = dto.Sex; + profile.ActivityLevel = dto.ActivityLevel; + profile.Goal = dto.Goal; + profile.AllergiesNotes = dto.AllergiesNotes; + profile.DislikedFoods = dto.DislikedFoods; + profile.MealsPerDayMask = dto.MealsPerDayMask; + profile.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(ct); + return MapProfile(profile); + } + + public async Task CreateSessionAsync( + Guid userId, + CreateDietGeneratorSessionDto dto, + CancellationToken ct = default) + { + var profile = await _db.DietUserProfiles.AsNoTracking().FirstOrDefaultAsync(p => p.UserId == userId, ct) + ?? throw new InvalidOperationException("Save your diet profile before starting a generator session."); + + var session = new DietGeneratorSession + { + UserId = userId, + Status = DietGeneratorStatuses.MacrosPending, + PlanStartDate = dto.PlanStartDate ?? DateOnly.FromDateTime(DateTime.UtcNow), + PlanDayCount = dto.PlanDayCount, + CalorieToleranceKcal = dto.CalorieToleranceKcal, + CreatedAt = DateTime.UtcNow, + }; + + _db.DietGeneratorSessions.Add(session); + await _db.SaveChangesAsync(ct); + + var calculated = TdeeCalculator.Calculate( + profile.HeightCm, profile.WeightKg, profile.Age, profile.Sex, profile.ActivityLevel, profile.Goal); + + session.ProposedCalories = calculated.Calories; + session.ProposedProteinG = calculated.ProteinG; + session.ProposedFatG = calculated.FatG; + session.ProposedCarbsG = calculated.CarbsG; + session.MacroRationale = calculated.Rationale; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + return await MapSessionAsync(session.Id, ct) ?? throw new InvalidOperationException("Session not found."); + } + + public async Task GetSessionAsync(Guid userId, int sessionId, CancellationToken ct = default) + { + await BackfillMissingDraftIngredientsAsync(userId, sessionId, ct); + return await MapSessionAsync(sessionId, userId, ct); + } + + public async Task ProposeMacrosAsync(Guid userId, int sessionId, CancellationToken ct = default) + { + var session = await GetEditableSessionAsync(userId, sessionId, ct); + if (session is null) return null; + + var profile = await _db.DietUserProfiles.AsNoTracking().FirstAsync(p => p.UserId == userId, ct); + var calculated = TdeeCalculator.Calculate( + profile.HeightCm, profile.WeightKg, profile.Age, profile.Sex, profile.ActivityLevel, profile.Goal); + + if (_openAi.IsConfigured) + { + try + { + var system = """ + You are a sports nutrition assistant. Respond ONLY with JSON: + {"calories":number,"proteinG":number,"fatG":number,"carbsG":number,"rationale":"string"} + Keep calories within 10% of the provided baseline. Use realistic macro splits. + """; + var user = JsonSerializer.Serialize(new + { + baseline = calculated, + profile = new + { + profile.HeightCm, + profile.WeightKg, + profile.Age, + profile.Sex, + profile.ActivityLevel, + profile.Goal, + profile.AllergiesNotes, + }, + }); + + var json = await _openAi.CompleteJsonAsync(system, user, OpenAiUseCase.Macros, ct); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + session.ProposedCalories = root.TryGetProperty("calories", out var c) + ? c.GetInt32() + : calculated.Calories; + session.ProposedProteinG = root.TryGetProperty("proteinG", out var p) + ? p.GetDecimal() + : calculated.ProteinG; + session.ProposedFatG = root.TryGetProperty("fatG", out var f) + ? f.GetDecimal() + : calculated.FatG; + session.ProposedCarbsG = root.TryGetProperty("carbsG", out var cb) + ? cb.GetDecimal() + : calculated.CarbsG; + session.MacroRationale = root.TryGetProperty("rationale", out var r) + ? r.GetString() + : calculated.Rationale; + } + catch + { + ApplyCalculatedMacros(session, calculated); + } + } + else + { + ApplyCalculatedMacros(session, calculated); + } + + session.Status = DietGeneratorStatuses.MacrosPending; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return await MapSessionAsync(sessionId, ct); + } + + public async Task AcceptMacrosAsync( + Guid userId, + int sessionId, + AcceptMacrosDto dto, + CancellationToken ct = default) + { + var session = await GetEditableSessionAsync(userId, sessionId, ct); + if (session is null) return null; + + if (dto.Calories.HasValue) session.ProposedCalories = dto.Calories; + if (dto.ProteinG.HasValue) session.ProposedProteinG = dto.ProteinG; + if (dto.FatG.HasValue) session.ProposedFatG = dto.FatG; + if (dto.CarbsG.HasValue) session.ProposedCarbsG = dto.CarbsG; + + session.Status = DietGeneratorStatuses.MacrosAccepted; + session.MacrosAcceptedAt = DateTime.UtcNow; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return await MapSessionAsync(sessionId, ct); + } + + public async Task GeneratePlanAsync(Guid userId, int sessionId, CancellationToken ct = default) + { + var session = await GetEditableSessionAsync(userId, sessionId, ct); + if (session is null || !session.ProposedCalories.HasValue) return null; + + var profile = await _db.DietUserProfiles.AsNoTracking().FirstAsync(p => p.UserId == userId, ct); + var pool = await LoadRecipePoolAsync(userId, ct); + if (pool.Count == 0) + { + throw new InvalidOperationException("No recipes with complete macros are available for planning."); + } + + var mealCategories = MealPlanOptimizer.MealCategoriesFromMask(profile.MealsPerDayMask); + var requiredUnique = MealPlanOptimizer.RequiredUniqueRecipes(session.PlanDayCount, mealCategories); + if (pool.Count < requiredUnique) + { + throw new InvalidOperationException( + $"Need at least {requiredUnique} unique recipes for a {session.PlanDayCount}-day plan " + + $"({mealCategories.Count} meals/day), but only {pool.Count} are available."); + } + + var start = session.PlanStartDate ?? DateOnly.FromDateTime(DateTime.UtcNow); + var usedRecipes = new HashSet(); + var allSlots = new List(); + + if (_openAi.IsConfigured) + { + try + { + allSlots = await GeneratePlanWithAiAsync(session, pool, mealCategories, start, usedRecipes, ct); + } + catch + { + allSlots = BuildGreedyPlan(session, pool, mealCategories, start, usedRecipes); + } + } + else + { + allSlots = BuildGreedyPlan(session, pool, mealCategories, start, usedRecipes); + } + + MealPlanOptimizer.EnsureWeeklyUniqueRecipes( + allSlots, + pool, + mealCategories, + session.ProposedCalories!.Value, + session.CalorieToleranceKcal); + + _db.DietGeneratorDraftMeals.RemoveRange( + await _db.DietGeneratorDraftMeals.Where(m => m.SessionId == sessionId).ToListAsync(ct)); + + var recipes = pool.ToDictionary(r => r.Id); + foreach (var slot in allSlots) + { + if (!recipes.TryGetValue(slot.RecipeId, out var recipe)) continue; + _db.DietGeneratorDraftMeals.Add(new DietGeneratorDraftMeal + { + SessionId = sessionId, + PlanDate = slot.PlanDate, + MealCategory = slot.MealCategory, + RecipeId = slot.RecipeId, + Portions = slot.Portions, + Calories = slot.TotalCalories, + ProteinG = Math.Round(slot.TotalProtein, 2), + FatG = Math.Round(slot.TotalFat, 2), + CarbsG = Math.Round(slot.TotalCarbs, 2), + GenerationVersion = 1, + }); + } + + session.Status = DietGeneratorStatuses.PlanDraft; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + var draftMeals = await _db.DietGeneratorDraftMeals + .Where(m => m.SessionId == sessionId) + .ToListAsync(ct); + foreach (var draftMeal in draftMeals) + { + await SyncDraftMealIngredientsAsync(draftMeal.Id, draftMeal.RecipeId, draftMeal.Portions, ct); + } + + await _db.SaveChangesAsync(ct); + return await MapSessionAsync(sessionId, ct); + } + + public async Task RegenerateMealAsync( + Guid userId, + int sessionId, + RegenerateDietMealDto dto, + CancellationToken ct = default) + { + var session = await GetEditableSessionAsync(userId, sessionId, ct); + if (session is null || !session.ProposedCalories.HasValue) return null; + + var draftMeals = await _db.DietGeneratorDraftMeals + .Where(m => m.SessionId == sessionId) + .ToListAsync(ct); + + var dayMeals = draftMeals.Where(m => m.PlanDate == dto.PlanDate).ToList(); + var target = session.ProposedCalories.Value; + var lockedCalories = dayMeals.Where(m => m.IsLocked && m.PlanDate == dto.PlanDate).Sum(m => m.Calories ?? 0); + var remaining = Math.Max(200, target - lockedCalories); + var unlockedSlots = dayMeals.Count(m => !m.IsLocked); + var slotTarget = unlockedSlots > 0 ? remaining / unlockedSlots : remaining; + + var pool = await LoadRecipePoolAsync(userId, ct); + + var existing = dayMeals.FirstOrDefault(m => m.MealCategory == dto.MealCategory); + if (existing is null) return null; + if (existing.IsLocked) return await MapSessionAsync(sessionId, ct); + + var excluded = draftMeals + .Where(m => m.Id != existing.Id) + .Select(m => m.RecipeId) + .ToHashSet(); + excluded.Add(existing.RecipeId); + + if (!MealPlanOptimizer.TryPickGreedyMeal(pool, dto.MealCategory, slotTarget, excluded, out var picked)) + { + throw new InvalidOperationException( + "No other recipes are available for this meal. All suitable options are already used in the plan."); + } + + var recipe = pool.First(r => r.Id == picked.RecipeId); + + existing.RecipeId = picked.RecipeId; + existing.Portions = picked.Portions; + existing.Calories = picked.TotalCalories; + existing.ProteinG = Math.Round(picked.TotalProtein, 2); + existing.FatG = Math.Round(picked.TotalFat, 2); + existing.CarbsG = Math.Round(picked.TotalCarbs, 2); + existing.GenerationVersion++; + + var slots = dayMeals.Select(m => new DraftMealSlot + { + PlanDate = m.PlanDate, + MealCategory = m.MealCategory, + RecipeId = m.RecipeId, + Portions = m.Portions, + CaloriesPerPortion = recipe.Calories, + FatPerPortion = recipe.Fat, + ProteinPerPortion = recipe.Protein, + CarbsPerPortion = recipe.Carbs, + IsLocked = m.IsLocked, + }).ToList(); + + foreach (var slot in slots) + { + var r = pool.First(x => x.Id == slot.RecipeId); + slot.CaloriesPerPortion = r.Calories; + slot.ProteinPerPortion = r.Protein; + slot.FatPerPortion = r.Fat; + slot.CarbsPerPortion = r.Carbs; + } + + MealPlanOptimizer.BalanceDay(slots, target, session.CalorieToleranceKcal); + foreach (var slot in slots) + { + var entity = dayMeals.First(m => m.MealCategory == slot.MealCategory); + var r = pool.First(x => x.Id == slot.RecipeId); + entity.Portions = slot.Portions; + entity.Calories = slot.TotalCalories; + entity.ProteinG = Math.Round(slot.TotalProtein, 2); + entity.FatG = Math.Round(slot.TotalFat, 2); + entity.CarbsG = Math.Round(slot.TotalCarbs, 2); + } + + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + await SyncDraftMealIngredientsAsync(existing.Id, existing.RecipeId, existing.Portions, ct); + await _db.SaveChangesAsync(ct); + return await MapSessionAsync(sessionId, ct); + } + + public async Task UpdateDraftMealAsync( + Guid userId, + int sessionId, + int mealId, + UpdateDraftMealDto dto, + CancellationToken ct = default) + { + var session = await GetEditableSessionAsync(userId, sessionId, ct); + if (session is null) return null; + + var meal = await _db.DietGeneratorDraftMeals + .FirstOrDefaultAsync(m => m.Id == mealId && m.SessionId == sessionId, ct); + if (meal is null) return null; + + if (dto.IsLocked.HasValue) meal.IsLocked = dto.IsLocked.Value; + if (dto.RecipeId.HasValue) + { + var recipe = await _db.Recipes.AsNoTracking() + .FirstOrDefaultAsync(r => r.Id == dto.RecipeId && r.IsActive && !r.IsDraft, ct); + if (recipe is null || !recipe.Calories.HasValue) return null; + meal.RecipeId = recipe.Id; + meal.Calories = (int)Math.Round(recipe.Calories.Value * meal.Portions); + meal.ProteinG = recipe.Protein.HasValue ? Math.Round(recipe.Protein.Value * meal.Portions, 2) : null; + meal.FatG = recipe.Fat.HasValue ? Math.Round(recipe.Fat.Value * meal.Portions, 2) : null; + meal.CarbsG = recipe.Carbs.HasValue ? Math.Round(recipe.Carbs.Value * meal.Portions, 2) : null; + } + + if (dto.Portions.HasValue) + { + meal.Portions = dto.Portions.Value; + var recipe = await _db.Recipes.AsNoTracking().FirstAsync(r => r.Id == meal.RecipeId, ct); + meal.Calories = recipe.Calories.HasValue + ? (int)Math.Round(recipe.Calories.Value * meal.Portions) + : null; + meal.ProteinG = recipe.Protein.HasValue ? Math.Round(recipe.Protein.Value * meal.Portions, 2) : null; + meal.FatG = recipe.Fat.HasValue ? Math.Round(recipe.Fat.Value * meal.Portions, 2) : null; + meal.CarbsG = recipe.Carbs.HasValue ? Math.Round(recipe.Carbs.Value * meal.Portions, 2) : null; + } + + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + await SyncDraftMealIngredientsAsync(meal.Id, meal.RecipeId, meal.Portions, ct); + await _db.SaveChangesAsync(ct); + return await MapSessionAsync(sessionId, ct); + } + + public async Task CommitAsync(Guid userId, int sessionId, CancellationToken ct = default) + { + var session = await _db.DietGeneratorSessions + .Include(s => s.DraftMeals) + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId, ct); + + if (session is null || session.Status != DietGeneratorStatuses.PlanDraft) return null; + if (!session.ProposedCalories.HasValue || session.DraftMeals.Count == 0) return null; + + var start = session.PlanStartDate ?? DateOnly.FromDateTime(DateTime.UtcNow); + var end = start.AddDays(session.PlanDayCount - 1); + + var goals = await _db.UserDailyGoals.FirstOrDefaultAsync(g => g.UserId == userId, ct); + if (goals is null) + { + goals = new UserDailyGoal { UserId = userId, CreatedAt = DateTime.UtcNow }; + _db.UserDailyGoals.Add(goals); + } + + goals.CalorieGoal = session.ProposedCalories; + goals.ProteinGoalG = session.ProposedProteinG; + goals.FatGoalG = session.ProposedFatG; + goals.CarbsGoalG = session.ProposedCarbsG; + goals.UpdatedAt = DateTime.UtcNow; + + var existingEntries = await _db.MealPlanEntries + .Where(e => e.UserId == userId && e.PlanDate >= start && e.PlanDate <= end) + .ToListAsync(ct); + _db.MealPlanEntries.RemoveRange(existingEntries); + + foreach (var draft in session.DraftMeals) + { + _db.MealPlanEntries.Add(new MealPlanEntry + { + UserId = userId, + PlanDate = draft.PlanDate, + MealCategory = draft.MealCategory, + RecipeId = draft.RecipeId, + Portions = draft.Portions, + CreatedAt = DateTime.UtcNow, + }); + } + + session.Status = DietGeneratorStatuses.Committed; + session.CommittedAt = DateTime.UtcNow; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + return new CommitDietPlanResultDto + { + SessionId = sessionId, + PlanStartDate = start, + PlanEndDate = end, + MealsWritten = session.DraftMeals.Count, + }; + } + + private async Task BackfillMissingDraftIngredientsAsync(Guid userId, int sessionId, CancellationToken ct) + { + var session = await _db.DietGeneratorSessions.AsNoTracking() + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId, ct); + if (session?.Status != DietGeneratorStatuses.PlanDraft) return; + + var meals = await _db.DietGeneratorDraftMeals + .Where(m => m.SessionId == sessionId) + .ToListAsync(ct); + + if (meals.Count == 0) return; + + foreach (var meal in meals) + { + await SyncDraftMealIngredientsAsync(meal.Id, meal.RecipeId, meal.Portions, ct); + } + + await _db.SaveChangesAsync(ct); + } + + private async Task SyncDraftMealIngredientsAsync( + int draftMealId, + int recipeId, + decimal portions, + CancellationToken ct) + { + var existing = await _db.DietGeneratorDraftMealIngredients + .Where(i => i.DraftMealId == draftMealId) + .ToListAsync(ct); + _db.DietGeneratorDraftMealIngredients.RemoveRange(existing); + + var recipeIngredients = await _db.Ingredients.AsNoTracking() + .Where(i => i.RecipeId == recipeId) + .OrderBy(i => i.SortOrder) + .ThenBy(i => i.Id) + .ToListAsync(ct); + + foreach (var ingredient in recipeIngredients) + { + var (scaledGrams, scaledUnit) = IngredientScalingHelper.ScaleForDietPlan( + ingredient.AmountGrams, + ingredient.Unit, + portions); + + _db.DietGeneratorDraftMealIngredients.Add(new DietGeneratorDraftMealIngredient + { + DraftMealId = draftMealId, + SourceIngredientId = ingredient.Id, + Name = ingredient.Name, + AmountGrams = scaledGrams, + Unit = scaledUnit, + SortOrder = ingredient.SortOrder, + }); + } + } + + private static void ApplyCalculatedMacros(DietGeneratorSession session, TdeeCalculator.MacroTargets calculated) + { + session.ProposedCalories = calculated.Calories; + session.ProposedProteinG = calculated.ProteinG; + session.ProposedFatG = calculated.FatG; + session.ProposedCarbsG = calculated.CarbsG; + session.MacroRationale = calculated.Rationale; + } + + private List BuildGreedyPlan( + DietGeneratorSession session, + IReadOnlyList pool, + IReadOnlyList mealCategories, + DateOnly start, + HashSet usedRecipes) + { + var allSlots = new List(); + for (var day = 0; day < session.PlanDayCount; day++) + { + var date = start.AddDays(day); + var daySlots = MealPlanOptimizer.BuildGreedyDay( + date, + pool, + mealCategories, + session.ProposedCalories!.Value, + session.CalorieToleranceKcal, + usedRecipes); + allSlots.AddRange(daySlots); + } + + return allSlots; + } + + private async Task> GeneratePlanWithAiAsync( + DietGeneratorSession session, + IReadOnlyList pool, + IReadOnlyList mealCategories, + DateOnly start, + HashSet usedRecipes, + CancellationToken ct) + { + var compact = pool.Take(120).Select(r => new + { + r.Id, + r.Name, + r.MealCategory, + r.Calories, + r.Protein, + r.Fat, + r.Carbs, + }); + + var system = """ + You are a meal planner. Pick ONLY recipe ids from the provided catalog. + Respond JSON: {"days":[{"date":"YYYY-MM-DD","meals":[{"mealCategory":0,"recipeId":1,"portions":1.0}]}]} + Each day must use allowed meal categories only. + Never reuse the same recipeId anywhere in the plan — each recipe may appear at most once across all days. + """; + var user = JsonSerializer.Serialize(new + { + targetCaloriesPerDay = session.ProposedCalories, + toleranceKcal = session.CalorieToleranceKcal, + mealCategories, + planStartDate = start.ToString("yyyy-MM-dd"), + planDayCount = session.PlanDayCount, + recipes = compact, + }); + + var json = await _openAi.CompleteJsonAsync(system, user, OpenAiUseCase.Plan, ct); + using var doc = JsonDocument.Parse(json); + var poolMap = pool.ToDictionary(r => r.Id); + var allSlots = new List(); + + if (!doc.RootElement.TryGetProperty("days", out var daysEl)) return BuildGreedyPlan(session, pool, mealCategories, start, usedRecipes); + + foreach (var dayEl in daysEl.EnumerateArray()) + { + var dateStr = dayEl.GetProperty("date").GetString(); + if (!DateOnly.TryParse(dateStr, out var date)) continue; + + var daySlots = new List(); + var perSlot = mealCategories.Count > 0 + ? session.ProposedCalories!.Value / mealCategories.Count + : session.ProposedCalories!.Value; + foreach (var mealEl in dayEl.GetProperty("meals").EnumerateArray()) + { + var category = mealEl.GetProperty("mealCategory").GetInt32(); + var recipeId = mealEl.GetProperty("recipeId").GetInt32(); + DraftMealSlot slot; + + if (!poolMap.TryGetValue(recipeId, out var recipe) || usedRecipes.Contains(recipeId)) + { + slot = MealPlanOptimizer.PickGreedyMeal(pool, category, perSlot, usedRecipes); + slot.PlanDate = date; + } + else + { + var portions = mealEl.TryGetProperty("portions", out var p) ? p.GetDecimal() : 1m; + slot = new DraftMealSlot + { + PlanDate = date, + MealCategory = category, + RecipeId = recipeId, + Portions = Math.Clamp(portions, 0.5m, 2.5m), + CaloriesPerPortion = recipe.Calories, + ProteinPerPortion = recipe.Protein, + FatPerPortion = recipe.Fat, + CarbsPerPortion = recipe.Carbs, + }; + } + + daySlots.Add(slot); + usedRecipes.Add(slot.RecipeId); + } + + if (daySlots.Count == 0) continue; + MealPlanOptimizer.BalanceDay(daySlots, session.ProposedCalories!.Value, session.CalorieToleranceKcal); + allSlots.AddRange(daySlots); + } + + if (allSlots.Count == 0) return BuildGreedyPlan(session, pool, mealCategories, start, usedRecipes); + return allSlots; + } + + private async Task> LoadRecipePoolAsync(Guid userId, CancellationToken ct) + { + return await _db.Recipes.AsNoTracking() + .Where(r => r.IsActive && !r.IsDraft && r.Calories.HasValue) + .Where(r => r.CreatedByUserId == null || r.CreatedByUserId == userId) + .Select(r => new RecipeCandidate( + r.Id, + r.Name, + r.MealCategory, + r.Calories!.Value, + r.Protein ?? 0, + r.Fat ?? 0, + r.Carbs ?? 0)) + .ToListAsync(ct); + } + + private async Task GetEditableSessionAsync(Guid userId, int sessionId, CancellationToken ct) => + await _db.DietGeneratorSessions.FirstOrDefaultAsync( + s => s.Id == sessionId && s.UserId == userId && s.Status != DietGeneratorStatuses.Committed, ct); + + private Task MapSessionAsync(int sessionId, CancellationToken ct) => + MapSessionAsync(sessionId, null, ct); + + private async Task MapSessionAsync(int sessionId, Guid? userId, CancellationToken ct) + { + var query = _db.DietGeneratorSessions.AsNoTracking() + .Include(s => s.DraftMeals) + .ThenInclude(m => m.Recipe) + .ThenInclude(r => r!.Steps) + .Include(s => s.DraftMeals) + .ThenInclude(m => m.PlannedIngredients) + .Where(s => s.Id == sessionId); + + if (userId.HasValue) query = query.Where(s => s.UserId == userId.Value); + + var session = await query.FirstOrDefaultAsync(ct); + if (session is null) return null; + + var meals = session.DraftMeals + .OrderBy(m => m.PlanDate) + .ThenBy(m => m.MealCategory) + .Select(m => new DietGeneratorDraftMealDto + { + Id = m.Id, + PlanDate = m.PlanDate, + MealCategory = m.MealCategory, + RecipeId = m.RecipeId, + RecipeName = m.Recipe?.Name ?? string.Empty, + Portions = m.Portions, + Calories = m.Calories, + ProteinG = m.ProteinG, + FatG = m.FatG, + CarbsG = m.CarbsG, + IsLocked = m.IsLocked, + PrepTimeMinutes = m.Recipe?.PrepTimeMinutes, + Ingredients = m.PlannedIngredients + .OrderBy(i => i.SortOrder) + .ThenBy(i => i.Id) + .Select(i => new DietGeneratorDraftMealIngredientDto + { + Id = i.Id, + Name = i.Name, + AmountGrams = i.AmountGrams, + Unit = i.Unit, + SortOrder = i.SortOrder, + SourceIngredientId = i.SourceIngredientId, + }) + .ToList(), + Steps = (m.Recipe?.Steps ?? []) + .OrderBy(s => s.StepNumber) + .Select(s => new DietGeneratorDraftMealStepDto + { + Id = s.Id, + StepNumber = s.StepNumber, + Description = s.Description, + }) + .ToList(), + }) + .ToList(); + + var summaries = meals + .GroupBy(m => m.PlanDate) + .Select(g => new DietGeneratorDaySummaryDto + { + PlanDate = g.Key, + TotalCalories = g.Sum(x => x.Calories ?? 0), + TargetCalories = session.ProposedCalories ?? 0, + WithinTolerance = session.ProposedCalories.HasValue && + Math.Abs(g.Sum(x => x.Calories ?? 0) - session.ProposedCalories.Value) <= session.CalorieToleranceKcal, + }) + .ToList(); + + return new DietGeneratorSessionDto + { + Id = session.Id, + Status = session.Status, + ProposedMacros = session.ProposedCalories.HasValue + ? new MacroProposalDto + { + Calories = session.ProposedCalories.Value, + ProteinG = session.ProposedProteinG ?? 0, + FatG = session.ProposedFatG ?? 0, + CarbsG = session.ProposedCarbsG ?? 0, + Rationale = session.MacroRationale, + } + : null, + PlanStartDate = session.PlanStartDate, + PlanDayCount = session.PlanDayCount, + CalorieToleranceKcal = session.CalorieToleranceKcal, + DraftMeals = meals, + DaySummaries = summaries, + }; + } + + private static DietUserProfileDto MapProfile(DietUserProfile profile) => new() + { + HeightCm = profile.HeightCm, + WeightKg = profile.WeightKg, + Age = profile.Age, + Sex = profile.Sex, + ActivityLevel = profile.ActivityLevel, + Goal = profile.Goal, + AllergiesNotes = profile.AllergiesNotes, + DislikedFoods = profile.DislikedFoods, + MealsPerDayMask = profile.MealsPerDayMask, + }; +} diff --git a/MealPlan.Api/Services/Generators/IngredientScalingHelper.cs b/MealPlan.Api/Services/Generators/IngredientScalingHelper.cs new file mode 100644 index 0000000..315ca7d --- /dev/null +++ b/MealPlan.Api/Services/Generators/IngredientScalingHelper.cs @@ -0,0 +1,64 @@ +namespace MealPlan.Api.Services.Generators; + +public static class IngredientScalingHelper +{ + private const decimal CountAmountThreshold = 10m; + + public static (decimal? AmountGrams, string? Unit) ScaleForDietPlan( + decimal? amountGrams, + string? unit, + decimal portions) + { + if (IsNonQuantifiedUnit(unit)) + { + return (amountGrams, unit); + } + + if (amountGrams is not > 0) + { + return (null, unit); + } + + if (IsCountUnit(unit) && amountGrams.Value <= CountAmountThreshold) + { + var gramsPerUnit = GramsPerCountUnit(unit!); + var totalGrams = amountGrams.Value * gramsPerUnit * portions; + return (Math.Round(totalGrams, 1), null); + } + + // AmountGrams stores weight in grams (even when the display unit says szt./ząbek). + return (Math.Round(amountGrams.Value * portions, 1), null); + } + + public static bool IsNonQuantifiedUnit(string? unit) + { + if (string.IsNullOrWhiteSpace(unit)) return false; + var u = unit.Trim().ToLowerInvariant(); + return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta" or "to taste"; + } + + private static bool IsCountUnit(string? unit) + { + if (string.IsNullOrWhiteSpace(unit)) return false; + var u = NormalizeUnit(unit); + return u is "szt" or "sztuka" or "sztuki" + or "ząbek" or "zabek" or "zabki" or "clove" or "cloves" + or "łyżka" or "lyzka" or "łyżeczka" or "lyzeczka" + or "jajko" or "jajka" or "egg" or "eggs"; + } + + private static decimal GramsPerCountUnit(string unit) + { + var u = NormalizeUnit(unit); + return u switch + { + "ząbek" or "zabek" or "zabki" or "clove" or "cloves" => 4m, + "łyżka" or "lyzka" => 15m, + "łyżeczka" or "lyzeczka" => 5m, + _ => 60m, // szt., jajko — typical single egg / piece default + }; + } + + private static string NormalizeUnit(string unit) => + unit.Trim().ToLowerInvariant().TrimEnd('.'); +} diff --git a/MealPlan.Api/Services/Generators/MealPlanOptimizer.cs b/MealPlan.Api/Services/Generators/MealPlanOptimizer.cs new file mode 100644 index 0000000..cda6942 --- /dev/null +++ b/MealPlan.Api/Services/Generators/MealPlanOptimizer.cs @@ -0,0 +1,221 @@ +namespace MealPlan.Api.Services.Generators; + +public record RecipeCandidate( + int Id, + string Name, + int MealCategory, + int Calories, + decimal Protein, + decimal Fat, + decimal Carbs); + +public class DraftMealSlot +{ + public DateOnly PlanDate { get; set; } + public int MealCategory { get; set; } + public int RecipeId { get; set; } + public decimal Portions { get; set; } = 1m; + public int CaloriesPerPortion { get; set; } + public decimal ProteinPerPortion { get; set; } + public decimal FatPerPortion { get; set; } + public decimal CarbsPerPortion { get; set; } + public bool IsLocked { get; set; } + + public int TotalCalories => (int)Math.Round(CaloriesPerPortion * Portions); + public decimal TotalProtein => ProteinPerPortion * Portions; + public decimal TotalFat => FatPerPortion * Portions; + public decimal TotalCarbs => CarbsPerPortion * Portions; + + public DraftMealSlot CloneForDate(DateOnly planDate) => + new() + { + PlanDate = planDate, + MealCategory = MealCategory, + RecipeId = RecipeId, + Portions = Portions, + CaloriesPerPortion = CaloriesPerPortion, + ProteinPerPortion = ProteinPerPortion, + FatPerPortion = FatPerPortion, + CarbsPerPortion = CarbsPerPortion, + IsLocked = IsLocked, + }; +} + +public static class MealPlanOptimizer +{ + public static IReadOnlyList MealCategoriesFromMask(int mask) => + Enumerable.Range(0, 5).Where(i => (mask & (1 << i)) != 0).ToList(); + + public static void BalanceDay(IList meals, int targetCalories, int tolerance) + { + if (meals.Count == 0) return; + + var unlocked = meals.Where(m => !m.IsLocked).ToList(); + if (unlocked.Count == 0) return; + + var current = meals.Sum(m => m.TotalCalories); + if (Math.Abs(current - targetCalories) <= tolerance) return; + + if (current <= 0) + { + foreach (var meal in unlocked) + { + meal.Portions = 1m; + } + + current = meals.Sum(m => m.TotalCalories); + } + + if (current <= 0) return; + + var scale = (decimal)targetCalories / current; + foreach (var meal in unlocked) + { + meal.Portions = Math.Clamp(Math.Round(meal.Portions * scale, 2), 0.5m, 2.5m); + } + + current = meals.Sum(m => m.TotalCalories); + if (Math.Abs(current - targetCalories) <= tolerance) return; + + var diff = targetCalories - current; + var adjustable = unlocked.OrderByDescending(m => m.CaloriesPerPortion).FirstOrDefault(); + if (adjustable is null || adjustable.CaloriesPerPortion <= 0) return; + + var portionDelta = diff / (decimal)adjustable.CaloriesPerPortion; + adjustable.Portions = Math.Clamp(Math.Round(adjustable.Portions + portionDelta, 2), 0.5m, 2.5m); + } + + public static bool IsDayWithinTolerance(IEnumerable meals, int targetCalories, int tolerance) => + Math.Abs(meals.Sum(m => m.TotalCalories) - targetCalories) <= tolerance; + + private static RecipeCandidate? FindGreedyCandidate( + IReadOnlyList pool, + int mealCategory, + int targetCaloriesForSlot, + HashSet excludedRecipeIds) + { + var candidates = pool + .Where(r => r.MealCategory == mealCategory && !excludedRecipeIds.Contains(r.Id)) + .OrderBy(r => Math.Abs(r.Calories - targetCaloriesForSlot)) + .ToList(); + + if (candidates.Count == 0) + { + candidates = pool + .Where(r => !excludedRecipeIds.Contains(r.Id)) + .OrderBy(r => Math.Abs(r.Calories - targetCaloriesForSlot)) + .ToList(); + } + + return candidates.FirstOrDefault(); + } + + public static bool TryPickGreedyMeal( + IReadOnlyList pool, + int mealCategory, + int targetCaloriesForSlot, + HashSet excludedRecipeIds, + out DraftMealSlot slot) + { + var recipe = FindGreedyCandidate(pool, mealCategory, targetCaloriesForSlot, excludedRecipeIds); + if (recipe is null) + { + slot = null!; + return false; + } + + var portions = recipe.Calories > 0 + ? Math.Clamp(Math.Round((decimal)targetCaloriesForSlot / recipe.Calories, 2), 0.5m, 2.5m) + : 1m; + + slot = new DraftMealSlot + { + MealCategory = mealCategory, + RecipeId = recipe.Id, + Portions = portions, + CaloriesPerPortion = recipe.Calories, + ProteinPerPortion = recipe.Protein, + FatPerPortion = recipe.Fat, + CarbsPerPortion = recipe.Carbs, + }; + return true; + } + + public static DraftMealSlot PickGreedyMeal( + IReadOnlyList pool, + int mealCategory, + int targetCaloriesForSlot, + HashSet excludedRecipeIds) + { + if (TryPickGreedyMeal(pool, mealCategory, targetCaloriesForSlot, excludedRecipeIds, out var slot)) + { + return slot; + } + + throw new InvalidOperationException("No recipes available for meal planning."); + } + + public static IReadOnlyList BuildGreedyDay( + DateOnly planDate, + IReadOnlyList pool, + IReadOnlyList mealCategories, + int targetCalories, + int tolerance, + HashSet excludedRecipeIds) + { + var perSlot = mealCategories.Count > 0 ? targetCalories / mealCategories.Count : targetCalories; + var meals = new List(); + + foreach (var category in mealCategories) + { + var slot = PickGreedyMeal(pool, category, perSlot, excludedRecipeIds); + slot.PlanDate = planDate; + meals.Add(slot); + excludedRecipeIds.Add(slot.RecipeId); + } + + BalanceDay(meals, targetCalories, tolerance); + return meals; + } + + public static int RequiredUniqueRecipes(int planDayCount, IReadOnlyList mealCategories) => + planDayCount * mealCategories.Count; + + /// + /// Replaces duplicate recipe picks so each recipe appears at most once in the plan. + /// + public static void EnsureWeeklyUniqueRecipes( + IList slots, + IReadOnlyList pool, + IReadOnlyList mealCategories, + int targetCalories, + int tolerance) + { + if (slots.Count == 0) return; + + var used = new HashSet(); + var perSlot = mealCategories.Count > 0 ? targetCalories / mealCategories.Count : targetCalories; + + foreach (var slot in slots.OrderBy(s => s.PlanDate).ThenBy(s => s.MealCategory)) + { + if (used.Add(slot.RecipeId)) continue; + + var replacement = PickGreedyMeal(pool, slot.MealCategory, perSlot, used); + CopyRecipeIntoSlot(slot, replacement); + used.Add(slot.RecipeId); + + var daySlots = slots.Where(s => s.PlanDate == slot.PlanDate).ToList(); + BalanceDay(daySlots, targetCalories, tolerance); + } + } + + private static void CopyRecipeIntoSlot(DraftMealSlot target, DraftMealSlot source) + { + target.RecipeId = source.RecipeId; + target.Portions = source.Portions; + target.CaloriesPerPortion = source.CaloriesPerPortion; + target.ProteinPerPortion = source.ProteinPerPortion; + target.FatPerPortion = source.FatPerPortion; + target.CarbsPerPortion = source.CarbsPerPortion; + } +} diff --git a/MealPlan.Api/Services/Generators/RecipeGeneratorService.cs b/MealPlan.Api/Services/Generators/RecipeGeneratorService.cs new file mode 100644 index 0000000..f09c289 --- /dev/null +++ b/MealPlan.Api/Services/Generators/RecipeGeneratorService.cs @@ -0,0 +1,683 @@ +using System.Text.Json; +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.Generators; +using MealPlan.Api.DTOs.Recipe; +using MealPlan.Api.Models; +using MealPlan.Api.Services.AI; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace MealPlan.Api.Services.Generators; + +public interface IRecipeGeneratorService +{ + Task CreateSessionAsync(Guid userId, RecipeGeneratorConstraintsDto dto, CancellationToken ct = default); + Task GetSessionAsync(Guid userId, int sessionId, CancellationToken ct = default); + Task GenerateAsync(Guid userId, int sessionId, GenerateRecipesRequestDto? request, CancellationToken ct = default); + Task RegeneratePartAsync(Guid userId, int sessionId, RegenerateRecipePartDto dto, CancellationToken ct = default); + Task UpdateDraftAsync(Guid userId, int sessionId, GeneratedRecipeDraftDto dto, CancellationToken ct = default); + Task RemoveDraftAsync(Guid userId, int sessionId, string draftId, CancellationToken ct = default); + Task CommitAsync(Guid userId, int sessionId, CommitRecipesRequestDto? request, CancellationToken ct = default); +} + +public class RecipeGeneratorService : IRecipeGeneratorService +{ + private readonly AppDbContext _db; + private readonly IOpenAiChatService _openAi; + private readonly ICatalogMatchingService _catalogMatching; + private readonly IRecipeManagementService _recipeManagement; + + public RecipeGeneratorService( + AppDbContext db, + IOpenAiChatService openAi, + ICatalogMatchingService catalogMatching, + IRecipeManagementService recipeManagement) + { + _db = db; + _openAi = openAi; + _catalogMatching = catalogMatching; + _recipeManagement = recipeManagement; + } + + public async Task CreateSessionAsync( + Guid userId, + RecipeGeneratorConstraintsDto dto, + CancellationToken ct = default) + { + var session = new RecipeGeneratorSession + { + UserId = userId, + Status = RecipeGeneratorStatuses.Draft, + ConstraintsJson = JsonSerializer.Serialize(dto), + CreatedAt = DateTime.UtcNow, + }; + _db.RecipeGeneratorSessions.Add(session); + await _db.SaveChangesAsync(ct); + return MapSession(session, dto, []); + } + + public async Task GetSessionAsync(Guid userId, int sessionId, CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions.AsNoTracking() + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId, ct); + if (session is null) return null; + var constraints = DeserializeConstraints(session.ConstraintsJson); + var drafts = DeserializeDrafts(session.DraftJson); + return MapSession(session, constraints, drafts); + } + + public async Task GenerateAsync( + Guid userId, + int sessionId, + GenerateRecipesRequestDto? request, + CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId && s.Status == RecipeGeneratorStatuses.Draft, ct); + if (session is null) return null; + + if (!_openAi.IsConfigured) + { + throw new InvalidOperationException( + "OpenAI is not configured. Set OpenAI__ApiKey in appsettings.Local.json."); + } + + var count = Math.Clamp(request?.Count ?? 3, 1, 8); + var constraints = DeserializeConstraints(session.ConstraintsJson); + if (string.IsNullOrWhiteSpace(constraints.Prompt)) + { + throw new InvalidOperationException("Recipe prompt is required."); + } + + var catalogSample = await _db.IngredientNutritionCatalog.AsNoTracking() + .Where(i => i.IsActive) + .OrderBy(i => i.Name) + .Take(80) + .Select(i => new { i.Name, i.NamePl, i.CaloriesPer100G, i.ProteinGPer100G, i.FatGPer100G, i.CarbsGPer100G }) + .ToListAsync(ct); + + var drafts = await GenerateDistinctDraftsAsync(constraints, catalogSample, count, ct); + if (drafts.Count == 0) + { + throw new InvalidOperationException("AI did not return any valid recipes. Try again."); + } + + var enriched = new List(); + foreach (var draft in drafts) + { + enriched.Add(await EnrichDraftAsync(EnsureDraftId(draft), constraints, ct)); + } + + session.DraftJson = SerializeDrafts(enriched); + session.GenerationVersion++; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return MapSession(session, constraints, enriched); + } + + public async Task RegeneratePartAsync( + Guid userId, + int sessionId, + RegenerateRecipePartDto dto, + CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId && s.Status == RecipeGeneratorStatuses.Draft, ct); + if (session is null) return null; + + var constraints = DeserializeConstraints(session.ConstraintsJson); + var drafts = DeserializeDrafts(session.DraftJson); + if (string.IsNullOrWhiteSpace(dto.DraftId)) + return MapSession(session, constraints, drafts); + + var index = drafts.FindIndex(d => d.DraftId == dto.DraftId); + if (index < 0) return null; + + var current = drafts[index]; + if (!_openAi.IsConfigured) + { + throw new InvalidOperationException( + "OpenAI is not configured. Set OpenAI__ApiKey in appsettings.Local.json."); + } + + var mode = dto.Mode.ToLowerInvariant(); + var system = mode switch + { + "steps" => """ + Rewrite ONLY cooking steps for the recipe. JSON: {"steps":[{"stepNumber":1,"description":"..."}]} + Keep ingredients unchanged. + """, + "ingredients" => """ + Rewrite ONLY ingredients list. JSON: {"ingredients":[{"name":"","amountGrams":0,"unit":null,"catalogName":""}]} + Keep name and steps unchanged. + """, + _ => """ + Improve the full recipe. Respond ONLY JSON with name, mealCategory, prepTimeMinutes, ingredients, steps. + Use JSON numbers (not strings) for mealCategory, prepTimeMinutes, amountGrams, stepNumber. + """, + }; + + var user = JsonSerializer.Serialize(new + { + instruction = dto.Instruction ?? constraints.Prompt, + current, + constraints, + }); + + var json = await _openAi.CompleteJsonAsync(system, user, OpenAiUseCase.Recipe, ct); + try + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + if (mode == "steps" && root.TryGetProperty("steps", out var stepsEl)) + { + current.Steps = ParseSteps(stepsEl); + } + else if (mode == "ingredients" && root.TryGetProperty("ingredients", out var ingEl)) + { + current.Ingredients = await ParseIngredientsAsync(ingEl, ct); + } + else + { + var parsed = ParseDraft(json, constraints.MealCategory); + parsed.DraftId = current.DraftId; + current = parsed; + } + } + catch (JsonException ex) + { + throw new InvalidOperationException("Could not parse AI recipe response. Try again.", ex); + } + + current = await EnrichDraftAsync(EnsureDraftId(current), constraints, ct); + drafts[index] = current; + session.DraftJson = SerializeDrafts(drafts); + session.GenerationVersion++; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return MapSession(session, constraints, drafts); + } + + public async Task UpdateDraftAsync( + Guid userId, + int sessionId, + GeneratedRecipeDraftDto dto, + CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId && s.Status == RecipeGeneratorStatuses.Draft, ct); + if (session is null) return null; + + var drafts = DeserializeDrafts(session.DraftJson); + var constraints = DeserializeConstraints(session.ConstraintsJson); + var enriched = await EnrichDraftAsync(EnsureDraftId(dto), constraints, ct); + var index = drafts.FindIndex(d => d.DraftId == enriched.DraftId); + if (index < 0) + { + drafts.Add(enriched); + } + else + { + drafts[index] = enriched; + } + + session.DraftJson = SerializeDrafts(drafts); + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + return MapSession(session, constraints, drafts); + } + + public async Task RemoveDraftAsync( + Guid userId, + int sessionId, + string draftId, + CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId && s.Status == RecipeGeneratorStatuses.Draft, ct); + if (session is null) return null; + + var drafts = DeserializeDrafts(session.DraftJson); + var removed = drafts.RemoveAll(d => d.DraftId == draftId); + if (removed == 0) return null; + + session.DraftJson = SerializeDrafts(drafts); + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + var constraints = DeserializeConstraints(session.ConstraintsJson); + return MapSession(session, constraints, drafts); + } + + public async Task CommitAsync( + Guid userId, + int sessionId, + CommitRecipesRequestDto? request, + CancellationToken ct = default) + { + var session = await _db.RecipeGeneratorSessions + .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId && s.Status == RecipeGeneratorStatuses.Draft, ct); + if (session is null || string.IsNullOrWhiteSpace(session.DraftJson)) return null; + + var drafts = DeserializeDrafts(session.DraftJson); + if (drafts.Count == 0) return null; + + var targetIds = request?.DraftIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Distinct().ToList(); + var toSave = targetIds is { Count: > 0 } + ? drafts.Where(d => targetIds.Contains(d.DraftId)).ToList() + : drafts.ToList(); + + if (toSave.Count == 0) return null; + + var saved = new List(); + foreach (var draft in toSave) + { + if (draft.Ingredients.Count == 0 || draft.Steps.Count == 0) continue; + + var input = new CreateRecipeDto + { + Name = draft.Name, + MealCategory = draft.MealCategory, + Calories = draft.Calories, + Protein = draft.Protein, + Fat = draft.Fat, + Carbs = draft.Carbs, + PrepTimeMinutes = draft.PrepTimeMinutes, + Ingredients = draft.Ingredients.Select((i, index) => new CreateIngredientDto + { + Name = i.Name, + AmountGrams = i.AmountGrams, + Unit = i.Unit, + SortOrder = index, + CatalogItemId = i.CatalogItemId, + }).ToList(), + Steps = draft.Steps.Select(s => new CreateRecipeStepDto + { + StepNumber = s.StepNumber, + Description = s.Description, + }).ToList(), + }; + + CommitRecipeResultDto savedEntry; + try + { + var created = await _recipeManagement.CreateRecipeAsync(input, ct); + var entity = await _db.Recipes.FirstAsync(r => r.Id == created.Id, ct); + entity.Source = "ai_generated"; + entity.CreatedByUserId = userId; + entity.IsDraft = false; + + savedEntry = new CommitRecipeResultDto + { + SessionId = sessionId, + RecipeId = created.Id, + RecipeName = created.Name, + DraftId = draft.DraftId, + }; + } + catch (DbUpdateException ex) when (ex.InnerException is PostgresException { SqlState: "23505" }) + { + throw new InvalidOperationException( + "Could not save the recipe because the database recipe ID sequence is out of sync. " + + "Run: dotnet run --project MealPlan.Api -- --fix-sequences", + ex); + } + + saved.Add(savedEntry); + } + + if (saved.Count == 0) return null; + + var savedIds = saved.Select(s => s.DraftId).ToHashSet(); + drafts.RemoveAll(d => savedIds.Contains(d.DraftId)); + session.DraftJson = SerializeDrafts(drafts); + session.SavedRecipeId = saved[^1].RecipeId; + session.Status = drafts.Count == 0 ? RecipeGeneratorStatuses.Saved : RecipeGeneratorStatuses.Draft; + session.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(ct); + + return new CommitRecipesResultDto + { + SessionId = sessionId, + Saved = saved, + }; + } + + private async Task EnrichDraftAsync( + GeneratedRecipeDraftDto draft, + RecipeGeneratorConstraintsDto constraints, + CancellationToken ct) + { + var ingredients = new List(); + foreach (var ing in draft.Ingredients) + { + var catalogId = ing.CatalogItemId; + if (!catalogId.HasValue) + { + catalogId = await _catalogMatching.ResolveCatalogItemIdAsync( + ing.CatalogName ?? ing.Name, ct); + } + + ingredients.Add(new GeneratedIngredientDto + { + Name = ing.Name, + AmountGrams = ing.AmountGrams, + Unit = ing.Unit, + CatalogItemId = catalogId, + CatalogName = ing.CatalogName, + IsLinked = catalogId.HasValue, + }); + } + + draft.Ingredients = ingredients; + draft.UnlinkedIngredientCount = ingredients.Count(i => + i.AmountGrams is > 0 && + !IsSpiceUnit(i.Unit) && + !i.IsLinked); + + await ScaleDraftToTargetCaloriesAsync(draft, constraints, ct); + await ApplyComputedMacrosAsync(draft, ct); + return draft; + } + + private async Task ApplyComputedMacrosAsync(GeneratedRecipeDraftDto draft, CancellationToken ct) + { + var macros = await ComputeMacrosAsync(draft.Ingredients, ct); + if (!macros.HasValue) return; + + draft.Calories = macros.Value.Calories; + draft.Protein = macros.Value.Protein; + draft.Fat = macros.Value.Fat; + draft.Carbs = macros.Value.Carbs; + } + + private async Task ScaleDraftToTargetCaloriesAsync( + GeneratedRecipeDraftDto draft, + RecipeGeneratorConstraintsDto constraints, + CancellationToken ct) + { + if (constraints.TargetCalories is not > 0) return; + + var target = constraints.TargetCalories.Value; + var tolerance = constraints.CalorieToleranceKcal > 0 ? constraints.CalorieToleranceKcal : 10; + var currentMacros = await ComputeMacrosAsync(draft.Ingredients, ct); + if (currentMacros is null || currentMacros.Value.Calories <= 0) return; + if (Math.Abs(currentMacros.Value.Calories - target) <= tolerance) return; + + var scale = Math.Clamp((decimal)target / currentMacros.Value.Calories, 0.25m, 4m); + foreach (var ing in draft.Ingredients) + { + if (ing.CatalogItemId is null || ing.AmountGrams is not > 0 || IsSpiceUnit(ing.Unit)) continue; + ing.AmountGrams = Math.Round(ing.AmountGrams.Value * scale, 1); + } + } + + private async Task<(int Calories, decimal Protein, decimal Fat, decimal Carbs)?> ComputeMacrosAsync( + IReadOnlyList ingredients, + CancellationToken ct) + { + var ids = ingredients.Where(i => i.CatalogItemId.HasValue && i.AmountGrams is > 0 && !IsSpiceUnit(i.Unit)) + .Select(i => i.CatalogItemId!.Value) + .Distinct() + .ToList(); + if (ids.Count == 0) return null; + + var catalog = await _db.IngredientNutritionCatalog.AsNoTracking() + .Where(i => ids.Contains(i.Id)) + .ToDictionaryAsync(i => i.Id, ct); + + int calories = 0; + decimal protein = 0, fat = 0, carbs = 0; + foreach (var ing in ingredients) + { + if (ing.CatalogItemId is null || ing.AmountGrams is not > 0 || IsSpiceUnit(ing.Unit)) continue; + if (!catalog.TryGetValue(ing.CatalogItemId.Value, out var cat)) continue; + var scale = ing.AmountGrams.Value / 100m; + calories += (int)Math.Round(cat.CaloriesPer100G * scale); + protein += cat.ProteinGPer100G * scale; + fat += cat.FatGPer100G * scale; + carbs += cat.CarbsGPer100G * scale; + } + + return (calories, Math.Round(protein, 2), Math.Round(fat, 2), Math.Round(carbs, 2)); + } + + private async Task> GenerateDistinctDraftsAsync( + RecipeGeneratorConstraintsDto constraints, + object catalogSample, + int count, + CancellationToken ct) + { + const int maxAttempts = 4; + var drafts = new List(); + + for (var attempt = 0; attempt < maxAttempts && drafts.Count < count; attempt++) + { + var need = count - drafts.Count; + var json = await RequestRecipeBatchJsonAsync( + constraints, + catalogSample, + need, + drafts.Select(d => d.Name).ToList(), + count, + ct); + var parsed = ParseDrafts(json, constraints.MealCategory); + + foreach (var draft in parsed) + { + if (IsDuplicateDraft(draft, drafts)) continue; + drafts.Add(draft); + if (drafts.Count >= count) break; + } + } + + return drafts.Take(count).ToList(); + } + + private async Task RequestRecipeBatchJsonAsync( + RecipeGeneratorConstraintsDto constraints, + object catalogSample, + int recipeCount, + IReadOnlyList excludeNames, + int totalRequested, + CancellationToken ct) + { + var system = """ + You create realistic Polish home-cooking recipes for a meal planning app. + Respond ONLY JSON: + {"recipes":[{"name":"string","mealCategory":0,"prepTimeMinutes":20,"ingredients":[{"name":"Polish name","amountGrams":100,"unit":null,"catalogName":"English catalog name"}],"steps":[{"stepNumber":1,"description":"..."}]}]} + Rules: + - Follow constraints.prompt strictly for every recipe (dish type, main ingredients, style). + - If constraints.targetCalories is set, each recipe MUST total about that many kcal using realistic ingredient grams. + Aim within ±constraints.calorieToleranceKcal kcal. Do NOT put the calorie target only in the name — scale amounts. + - Use catalogName from the provided catalog when possible; grams required for macro ingredients. + - Spices may use unit "do smaku" with null amountGrams. + - mealCategory: 0=Breakfast, 1=SecondBreakfast, 2=Lunch, 3=Dinner, 4=Snack. + - Return exactly recipeCount recipes in the recipes array. + - Each recipe MUST be clearly different (unique name, different ingredient mix, different preparation). + - Do NOT repeat the same recipe with only a number suffix. + - Do NOT copy any name from excludeExistingNames. + """; + var user = JsonSerializer.Serialize(new + { + constraints, + catalog = catalogSample, + recipeCount, + totalRequested, + excludeExistingNames = excludeNames, + }); + return await _openAi.CompleteJsonAsync(system, user, OpenAiUseCase.Recipe, ct); + } + + private static bool IsDuplicateDraft(GeneratedRecipeDraftDto candidate, IReadOnlyList existing) => + existing.Any(existingDraft => DraftFingerprint(existingDraft) == DraftFingerprint(candidate)); + + private static string DraftFingerprint(GeneratedRecipeDraftDto draft) + { + var ingredients = string.Join( + "|", + draft.Ingredients + .OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .Select(i => $"{i.Name.Trim().ToLowerInvariant()}:{i.AmountGrams}:{i.Unit}")); + return $"{NormalizeRecipeName(draft.Name)}::{ingredients}"; + } + + private static string NormalizeRecipeName(string name) => + name.Trim().ToLowerInvariant(); + + private GeneratedRecipeDraftDto ParseDraft(string json, int defaultCategory) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + return EnsureDraftId(new GeneratedRecipeDraftDto + { + Name = root.TryGetProperty("name", out var n) ? AiRecipeJsonParser.GetString(n) ?? "Nowy przepis" : "Nowy przepis", + MealCategory = root.TryGetProperty("mealCategory", out var mc) + ? AiRecipeJsonParser.GetInt32(mc, defaultCategory) + : defaultCategory, + PrepTimeMinutes = root.TryGetProperty("prepTimeMinutes", out var pt) + ? AiRecipeJsonParser.GetNullableInt32(pt) + : null, + Ingredients = root.TryGetProperty("ingredients", out var ing) ? ParseIngredientsSync(ing) : [], + Steps = root.TryGetProperty("steps", out var st) ? ParseSteps(st) : [], + }); + } + + private List ParseDrafts(string json, int defaultCategory) + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var list = new List(); + + if (root.TryGetProperty("recipes", out var recipesEl) && recipesEl.ValueKind == JsonValueKind.Array) + { + foreach (var item in recipesEl.EnumerateArray()) + { + list.Add(EnsureDraftId(ParseDraft(item.GetRawText(), defaultCategory))); + } + } + else if (root.ValueKind == JsonValueKind.Array) + { + foreach (var item in root.EnumerateArray()) + { + list.Add(EnsureDraftId(ParseDraft(item.GetRawText(), defaultCategory))); + } + } + else if (root.TryGetProperty("name", out _)) + { + list.Add(ParseDraft(json, defaultCategory)); + } + + return list; + } + + private List ParseIngredientsSync(JsonElement ingEl) + { + var list = new List(); + foreach (var item in ingEl.EnumerateArray()) + { + list.Add(new GeneratedIngredientDto + { + Name = item.TryGetProperty("name", out var nameEl) + ? AiRecipeJsonParser.GetString(nameEl) ?? "" + : "", + AmountGrams = item.TryGetProperty("amountGrams", out var g) + ? AiRecipeJsonParser.GetNullableDecimal(g) + : null, + Unit = item.TryGetProperty("unit", out var u) && u.ValueKind != JsonValueKind.Null + ? AiRecipeJsonParser.GetString(u) + : null, + CatalogName = item.TryGetProperty("catalogName", out var c) + ? AiRecipeJsonParser.GetString(c) + : null, + }); + } + + return list; + } + + private async Task> ParseIngredientsAsync(JsonElement ingEl, CancellationToken ct) + { + var list = ParseIngredientsSync(ingEl); + foreach (var item in list) + { + item.CatalogItemId = await _catalogMatching.ResolveCatalogItemIdAsync(item.CatalogName ?? item.Name, ct); + item.IsLinked = item.CatalogItemId.HasValue; + } + + return list; + } + + private static List ParseSteps(JsonElement stepsEl) + { + var list = new List(); + foreach (var item in stepsEl.EnumerateArray()) + { + var stepNumber = item.TryGetProperty("stepNumber", out var stepEl) + ? AiRecipeJsonParser.GetInt32(stepEl, list.Count + 1) + : list.Count + 1; + list.Add(new GeneratedStepDto + { + StepNumber = stepNumber, + Description = item.TryGetProperty("description", out var descEl) + ? AiRecipeJsonParser.GetString(descEl) ?? "" + : "", + }); + } + + return list; + } + + private static bool IsSpiceUnit(string? unit) + { + if (string.IsNullOrWhiteSpace(unit)) return false; + var u = unit.Trim().ToLowerInvariant(); + return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta"; + } + + private static RecipeGeneratorConstraintsDto DeserializeConstraints(string json) => + JsonSerializer.Deserialize(json) ?? new RecipeGeneratorConstraintsDto(); + + private static List DeserializeDrafts(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return []; + + var trimmed = json.TrimStart(); + if (trimmed.StartsWith('[')) + { + return (JsonSerializer.Deserialize>(json) ?? []) + .Select(EnsureDraftId) + .ToList(); + } + + var single = JsonSerializer.Deserialize(json); + return single is null ? [] : [EnsureDraftId(single)]; + } + + private static string SerializeDrafts(IReadOnlyList drafts) => + JsonSerializer.Serialize(drafts); + + private static GeneratedRecipeDraftDto EnsureDraftId(GeneratedRecipeDraftDto draft) + { + if (string.IsNullOrWhiteSpace(draft.DraftId)) + { + draft.DraftId = Guid.NewGuid().ToString(); + } + + return draft; + } + + private static RecipeGeneratorSessionDto MapSession( + RecipeGeneratorSession session, + RecipeGeneratorConstraintsDto constraints, + IReadOnlyList drafts) => new() + { + Id = session.Id, + Status = session.Status, + Constraints = constraints, + Draft = drafts.FirstOrDefault(), + Drafts = drafts, + SavedRecipeId = session.SavedRecipeId, + GenerationVersion = session.GenerationVersion, + }; +} diff --git a/MealPlan.Api/Services/Generators/TdeeCalculator.cs b/MealPlan.Api/Services/Generators/TdeeCalculator.cs new file mode 100644 index 0000000..9533c29 --- /dev/null +++ b/MealPlan.Api/Services/Generators/TdeeCalculator.cs @@ -0,0 +1,59 @@ +namespace MealPlan.Api.Services.Generators; + +public static class TdeeCalculator +{ + public record MacroTargets(int Calories, decimal ProteinG, decimal FatG, decimal CarbsG, string Rationale); + + public static MacroTargets Calculate( + decimal heightCm, + decimal weightKg, + int age, + string sex, + string activityLevel, + string goal) + { + var bmr = sex.ToLowerInvariant() switch + { + "female" => 10m * weightKg + 6.25m * heightCm - 5m * age - 161m, + _ => 10m * weightKg + 6.25m * heightCm - 5m * age + 5m, + }; + + var multiplier = activityLevel.ToLowerInvariant() switch + { + "sedentary" => 1.2m, + "light" => 1.375m, + "moderate" => 1.55m, + "active" => 1.725m, + "very_active" => 1.9m, + _ => 1.55m, + }; + + var tdee = bmr * multiplier; + var adjustment = goal.ToLowerInvariant() switch + { + "lose_weight" => -500, + "gain_muscle" => 300, + "recomp" => -200, + _ => 0, + }; + + var calories = Math.Max(1200, (int)Math.Round(tdee + adjustment)); + + var (proteinPct, fatPct, carbsPct) = goal.ToLowerInvariant() switch + { + "gain_muscle" => (0.30m, 0.25m, 0.45m), + "lose_weight" => (0.35m, 0.25m, 0.40m), + "recomp" => (0.32m, 0.28m, 0.40m), + _ => (0.25m, 0.30m, 0.45m), + }; + + var proteinG = Math.Round(calories * proteinPct / 4m, 1); + var fatG = Math.Round(calories * fatPct / 9m, 1); + var carbsG = Math.Round(calories * carbsPct / 4m, 1); + + var rationale = + $"TDEE ≈ {(int)Math.Round(tdee)} kcal ({activityLevel}, {goal}). Target {calories} kcal after goal adjustment."; + + return new MacroTargets(calories, proteinG, fatG, carbsG, rationale); + } +} diff --git a/MealPlan.Api/Services/IDietTrackingService.cs b/MealPlan.Api/Services/IDietTrackingService.cs new file mode 100644 index 0000000..4eff5bb --- /dev/null +++ b/MealPlan.Api/Services/IDietTrackingService.cs @@ -0,0 +1,33 @@ +using MealPlan.Api.DTOs.Diet; + +namespace MealPlan.Api.Services; + +public interface IDietTrackingService +{ + Task GetGoalsAsync(Guid userId, CancellationToken ct = default); + Task UpsertGoalsAsync(Guid userId, UserDailyGoalsDto dto, CancellationToken ct = default); + Task GetDailySummaryAsync(Guid userId, DateOnly date, CancellationToken ct = default); + Task> GetMealsAsync(Guid userId, DateOnly date, CancellationToken ct = default); + Task LogMealAsync(Guid userId, LogMealDto dto, CancellationToken ct = default); + Task PreviewMealAsync(Guid userId, DateOnly date, LogMealDto dto, CancellationToken ct = default); + Task> GetMealSuggestionsAsync(Guid userId, DateOnly date, int limit = 5, CancellationToken ct = default); + Task DeleteMealAsync(Guid userId, int id, CancellationToken ct = default); + Task> GetDrinkCatalogAsync(CancellationToken ct = default); + Task> GetDrinksAsync(Guid userId, DateOnly date, CancellationToken ct = default); + Task LogDrinkAsync(Guid userId, LogDrinkDto dto, CancellationToken ct = default); + Task DeleteDrinkAsync(Guid userId, int id, CancellationToken ct = default); + Task> GetRemindersAsync(Guid userId, CancellationToken ct = default); + Task CreateReminderAsync(Guid userId, CreateDietReminderDto dto, CancellationToken ct = default); + Task UpdateReminderAsync(Guid userId, int id, UpdateDietReminderDto dto, CancellationToken ct = default); + Task DeleteReminderAsync(Guid userId, int id, CancellationToken ct = default); + Task> GetDueRemindersAsync(Guid userId, DateOnly? date, TimeOnly? time, CancellationToken ct = default); + Task> GetHistoryAsync(Guid userId, DateOnly from, DateOnly to, CancellationToken ct = default); + Task> GetRecentMealsAsync(Guid userId, int limit = 10, CancellationToken ct = default); + Task CopyYesterdayMealsAsync(Guid userId, DateOnly targetDate, CancellationToken ct = default); + Task> GetFavoriteRecipesAsync(Guid userId, CancellationToken ct = default); + Task AddFavoriteRecipeAsync(Guid userId, int recipeId, CancellationToken ct = default); + Task RemoveFavoriteRecipeAsync(Guid userId, int recipeId, CancellationToken ct = default); + Task> GetFavoriteCatalogItemsAsync(Guid userId, CancellationToken ct = default); + Task AddFavoriteCatalogItemAsync(Guid userId, int catalogItemId, CancellationToken ct = default); + Task RemoveFavoriteCatalogItemAsync(Guid userId, int catalogItemId, CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/IIngredientCatalogService.cs b/MealPlan.Api/Services/IIngredientCatalogService.cs new file mode 100644 index 0000000..074736e --- /dev/null +++ b/MealPlan.Api/Services/IIngredientCatalogService.cs @@ -0,0 +1,16 @@ +using MealPlan.Api.DTOs.IngredientCatalog; + +namespace MealPlan.Api.Services; + +public interface IIngredientCatalogService +{ + Task> GetCategoriesAsync(CancellationToken ct = default); + Task> GetItemsAsync( + string? categoryCode, + string? search, + CancellationToken ct = default); + Task GetItemByIdAsync(int id, CancellationToken ct = default); + Task CreateItemAsync(UpsertIngredientNutritionItemDto dto, CancellationToken ct = default); + Task UpdateItemAsync(int id, UpsertIngredientNutritionItemDto dto, CancellationToken ct = default); + Task DeleteItemAsync(int id, CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/IMealPlanService.cs b/MealPlan.Api/Services/IMealPlanService.cs new file mode 100644 index 0000000..1551680 --- /dev/null +++ b/MealPlan.Api/Services/IMealPlanService.cs @@ -0,0 +1,11 @@ +using MealPlan.Api.DTOs.MealPlan; + +namespace MealPlan.Api.Services; + +public interface IMealPlanService +{ + Task> GetEntriesAsync(Guid userId, DateOnly from, DateOnly to, CancellationToken ct = default); + Task UpsertEntryAsync(Guid userId, UpsertMealPlanEntryDto dto, CancellationToken ct = default); + Task DeleteEntryAsync(Guid userId, int id, CancellationToken ct = default); + Task> GetShoppingListAsync(Guid userId, DateOnly from, DateOnly to, CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/IRecipeExcelService.cs b/MealPlan.Api/Services/IRecipeExcelService.cs new file mode 100644 index 0000000..ab696e8 --- /dev/null +++ b/MealPlan.Api/Services/IRecipeExcelService.cs @@ -0,0 +1,12 @@ +using MealPlan.Api.DTOs.Recipe; + +namespace MealPlan.Api.Services; + +public interface IRecipeExcelService +{ + /// Generates a blank .xlsx template with Recipes, Ingredients, and Steps sheets. + byte[] GenerateTemplate(); + + /// Parses an uploaded workbook and creates recipes in the database. + Task ImportAsync(Stream fileStream, CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/IRecipeManagementService.cs b/MealPlan.Api/Services/IRecipeManagementService.cs new file mode 100644 index 0000000..fd5c787 --- /dev/null +++ b/MealPlan.Api/Services/IRecipeManagementService.cs @@ -0,0 +1,12 @@ +using MealPlan.Api.DTOs.Recipe; + +namespace MealPlan.Api.Services; + +public interface IRecipeManagementService +{ + Task CreateRecipeAsync(CreateRecipeDto dto, CancellationToken ct = default); + + Task UpdateRecipeAsync(int id, CreateRecipeDto dto, CancellationToken ct = default); + + Task RecalculateAllMacrosFromCatalogAsync(CancellationToken ct = default); +} diff --git a/MealPlan.Api/Services/IRecipeService.cs b/MealPlan.Api/Services/IRecipeService.cs index 871683e..5fba508 100644 --- a/MealPlan.Api/Services/IRecipeService.cs +++ b/MealPlan.Api/Services/IRecipeService.cs @@ -4,7 +4,7 @@ namespace MealPlan.Api.Services; public interface IRecipeService { - Task> GetRecipesAsync(int? category, string? search, CancellationToken ct = default); + Task> GetRecipesAsync(int? category, string? search, string? ingredient, CancellationToken ct = default); Task GetRecipeByIdAsync(int id, CancellationToken ct = default); diff --git a/MealPlan.Api/Services/IngredientCatalogService.cs b/MealPlan.Api/Services/IngredientCatalogService.cs new file mode 100644 index 0000000..40f873d --- /dev/null +++ b/MealPlan.Api/Services/IngredientCatalogService.cs @@ -0,0 +1,153 @@ +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.IngredientCatalog; +using MealPlan.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services; + +public class IngredientCatalogService : IIngredientCatalogService +{ + private readonly AppDbContext _db; + + public IngredientCatalogService(AppDbContext db) => _db = db; + + public async Task> GetCategoriesAsync(CancellationToken ct = default) + { + return await _db.IngredientCategories.AsNoTracking() + .Where(c => c.IsActive) + .OrderBy(c => c.SortOrder) + .ThenBy(c => c.Name) + .Select(c => new IngredientCategoryDto + { + Id = c.Id, + Code = c.Code, + Name = c.Name, + ItemCount = c.Items.Count(i => i.IsActive), + }) + .ToListAsync(ct); + } + + public async Task> GetItemsAsync( + string? categoryCode, + string? search, + CancellationToken ct = default) + { + var query = _db.IngredientNutritionCatalog.AsNoTracking() + .Include(i => i.Category) + .Where(i => i.IsActive && i.Category!.IsActive); + + if (!string.IsNullOrWhiteSpace(categoryCode)) + { + var code = categoryCode.Trim().ToLowerInvariant(); + query = query.Where(i => i.Category!.Code == code); + } + + if (!string.IsNullOrWhiteSpace(search)) + { + var term = search.Trim().ToLowerInvariant(); + query = query.Where(i => + i.Name.ToLower().Contains(term) || + (i.NamePl != null && i.NamePl.ToLower().Contains(term))); + } + + var rows = await query + .OrderBy(i => i.Category!.SortOrder) + .ThenBy(i => i.SortOrder) + .ThenBy(i => i.Name) + .ToListAsync(ct); + + return rows.Select(MapItem).ToList(); + } + + public async Task GetItemByIdAsync(int id, CancellationToken ct = default) + { + var row = await _db.IngredientNutritionCatalog.AsNoTracking() + .Include(i => i.Category) + .FirstOrDefaultAsync(i => i.Id == id && i.IsActive, ct); + return row is null ? null : MapItem(row); + } + + public async Task CreateItemAsync( + UpsertIngredientNutritionItemDto dto, + CancellationToken ct = default) + { + var category = await _db.IngredientCategories.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == dto.CategoryId && c.IsActive, ct); + if (category is null) return null; + + var entity = new IngredientNutritionCatalogItem + { + CategoryId = dto.CategoryId, + Name = dto.Name.Trim(), + NamePl = string.IsNullOrWhiteSpace(dto.NamePl) ? null : dto.NamePl.Trim(), + CaloriesPer100G = dto.CaloriesPer100G, + ProteinGPer100G = dto.ProteinGPer100G, + FatGPer100G = dto.FatGPer100G, + CarbsGPer100G = dto.CarbsGPer100G, + FiberGPer100G = dto.FiberGPer100G, + SortOrder = dto.SortOrder, + IsActive = dto.IsActive, + }; + + _db.IngredientNutritionCatalog.Add(entity); + await _db.SaveChangesAsync(ct); + + entity.Category = category; + return MapItem(entity); + } + + public async Task UpdateItemAsync( + int id, + UpsertIngredientNutritionItemDto dto, + CancellationToken ct = default) + { + var entity = await _db.IngredientNutritionCatalog + .Include(i => i.Category) + .FirstOrDefaultAsync(i => i.Id == id, ct); + if (entity is null) return null; + + var category = await _db.IngredientCategories.AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == dto.CategoryId && c.IsActive, ct); + if (category is null) return null; + + entity.CategoryId = dto.CategoryId; + entity.Name = dto.Name.Trim(); + entity.NamePl = string.IsNullOrWhiteSpace(dto.NamePl) ? null : dto.NamePl.Trim(); + entity.CaloriesPer100G = dto.CaloriesPer100G; + entity.ProteinGPer100G = dto.ProteinGPer100G; + entity.FatGPer100G = dto.FatGPer100G; + entity.CarbsGPer100G = dto.CarbsGPer100G; + entity.FiberGPer100G = dto.FiberGPer100G; + entity.SortOrder = dto.SortOrder; + entity.IsActive = dto.IsActive; + + await _db.SaveChangesAsync(ct); + entity.Category = category; + return MapItem(entity); + } + + public async Task DeleteItemAsync(int id, CancellationToken ct = default) + { + var entity = await _db.IngredientNutritionCatalog.FirstOrDefaultAsync(i => i.Id == id, ct); + if (entity is null) return false; + + entity.IsActive = false; + await _db.SaveChangesAsync(ct); + return true; + } + + private static IngredientNutritionItemDto MapItem(IngredientNutritionCatalogItem item) => new() + { + Id = item.Id, + CategoryId = item.CategoryId, + CategoryCode = item.Category?.Code ?? string.Empty, + CategoryName = item.Category?.Name ?? string.Empty, + Name = item.Name, + NamePl = item.NamePl, + CaloriesPer100G = item.CaloriesPer100G, + ProteinGPer100G = item.ProteinGPer100G, + FatGPer100G = item.FatGPer100G, + CarbsGPer100G = item.CarbsGPer100G, + FiberGPer100G = item.FiberGPer100G, + }; +} diff --git a/MealPlan.Api/Services/MealPlanService.cs b/MealPlan.Api/Services/MealPlanService.cs new file mode 100644 index 0000000..a1bc1db --- /dev/null +++ b/MealPlan.Api/Services/MealPlanService.cs @@ -0,0 +1,197 @@ +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.MealPlan; +using MealPlan.Api.Services.Generators; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services; + +public class MealPlanService : IMealPlanService +{ + private readonly AppDbContext _db; + private readonly IRecipeService _recipeService; + + public MealPlanService(AppDbContext db, IRecipeService recipeService) + { + _db = db; + _recipeService = recipeService; + } + + public async Task> GetEntriesAsync( + Guid userId, + DateOnly from, + DateOnly to, + CancellationToken ct = default) + { + return await _db.MealPlanEntries.AsNoTracking() + .Include(e => e.Recipe) + .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to) + .OrderBy(e => e.PlanDate) + .ThenBy(e => e.MealCategory) + .Select(e => new MealPlanEntryDto + { + Id = e.Id, + PlanDate = e.PlanDate, + MealCategory = e.MealCategory, + RecipeId = e.RecipeId, + RecipeName = e.Recipe!.Name, + Portions = e.Portions, + Calories = e.Recipe!.Calories, + }) + .ToListAsync(ct); + } + + public async Task UpsertEntryAsync( + Guid userId, + UpsertMealPlanEntryDto dto, + CancellationToken ct = default) + { + var recipeExists = await _db.Recipes.AnyAsync(r => r.Id == dto.RecipeId && r.IsActive, ct); + if (!recipeExists) return null; + + var existing = await _db.MealPlanEntries + .Include(e => e.Recipe) + .FirstOrDefaultAsync( + e => e.UserId == userId && e.PlanDate == dto.PlanDate && e.MealCategory == dto.MealCategory, + ct); + + if (existing is null) + { + existing = new Models.MealPlanEntry + { + UserId = userId, + PlanDate = dto.PlanDate, + MealCategory = dto.MealCategory, + RecipeId = dto.RecipeId, + Portions = dto.Portions, + CreatedAt = DateTime.UtcNow, + }; + _db.MealPlanEntries.Add(existing); + } + else + { + existing.RecipeId = dto.RecipeId; + existing.Portions = dto.Portions; + existing.UpdatedAt = DateTime.UtcNow; + if (existing.Recipe is null) + { + await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct); + } + } + + await _db.SaveChangesAsync(ct); + await _db.Entry(existing).Reference(e => e.Recipe).LoadAsync(ct); + + return new MealPlanEntryDto + { + Id = existing.Id, + PlanDate = existing.PlanDate, + MealCategory = existing.MealCategory, + RecipeId = existing.RecipeId, + RecipeName = existing.Recipe?.Name ?? string.Empty, + Portions = existing.Portions, + Calories = existing.Recipe?.Calories, + }; + } + + public async Task DeleteEntryAsync(Guid userId, int id, CancellationToken ct = default) + { + var entry = await _db.MealPlanEntries.FirstOrDefaultAsync(e => e.Id == id && e.UserId == userId, ct); + if (entry is null) return false; + _db.MealPlanEntries.Remove(entry); + await _db.SaveChangesAsync(ct); + return true; + } + + public async Task> GetShoppingListAsync( + Guid userId, + DateOnly from, + DateOnly to, + CancellationToken ct = default) + { + var entries = await _db.MealPlanEntries.AsNoTracking() + .Where(e => e.UserId == userId && e.PlanDate >= from && e.PlanDate <= to) + .ToListAsync(ct); + + var recipeIds = entries.Select(e => e.RecipeId).Distinct().ToList(); + var recipes = new List(); + foreach (var id in recipeIds) + { + var recipe = await _recipeService.GetRecipeByIdAsync(id, ct); + if (recipe is not null) recipes.Add(recipe); + } + + var buckets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in entries) + { + var recipe = recipes.FirstOrDefault(r => r.Id == entry.RecipeId); + if (recipe is null) continue; + + foreach (var ing in recipe.Ingredients) + { + if (IngredientScalingHelper.IsNonQuantifiedUnit(ing.Unit)) + continue; + + var (scaledGrams, _) = IngredientScalingHelper.ScaleForDietPlan( + ing.AmountGrams, ing.Unit, entry.Portions); + + if (scaledGrams is not > 0) + continue; + + var key = $"g:{NormalizeName(ing.Name)}"; + + if (!buckets.TryGetValue(key, out var bucket)) + { + bucket = new ShoppingBucket(ing.Name, null); + buckets[key] = bucket; + } + + bucket.Add(recipe.Name, scaledGrams, null); + } + } + + return buckets.Values + .Select(b => new ShoppingListItemDto + { + Name = b.Name, + Unit = b.Unit, + TotalGrams = b.TotalGrams, + TotalAmount = b.TotalAmount, + Breakdown = b.Breakdown, + }) + .OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static string NormalizeName(string name) => name.Trim().ToLowerInvariant(); + + private sealed class ShoppingBucket + { + public string Name { get; } + public string? Unit { get; } + public decimal? TotalGrams { get; private set; } + public decimal? TotalAmount { get; private set; } + public List Breakdown { get; } = new(); + + public ShoppingBucket(string name, string? unit) + { + Name = name; + Unit = unit; + } + + public void Add(string recipeName, decimal? grams, decimal? amount) + { + if (Unit is null && grams.HasValue) + { + TotalGrams = (TotalGrams ?? 0) + grams.Value; + Breakdown.Add($"{recipeName}: {grams.Value:0.##}g"); + } + else if (amount.HasValue) + { + TotalAmount = (TotalAmount ?? 0) + amount.Value; + var suffix = Unit is null ? "g" : $" {Unit}"; + Breakdown.Add($"{recipeName}: {amount.Value:0.##}{suffix}"); + } + } + } +} diff --git a/MealPlan.Api/Services/RecipeExcelService.cs b/MealPlan.Api/Services/RecipeExcelService.cs new file mode 100644 index 0000000..c278166 --- /dev/null +++ b/MealPlan.Api/Services/RecipeExcelService.cs @@ -0,0 +1,306 @@ +using ClosedXML.Excel; +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.Recipe; +using MealPlan.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services; + +/// +/// Imports recipes from a three-sheet Excel workbook (Recipes, Ingredients, Steps). +/// Also generates a downloadable template in the same format. +/// +public class RecipeExcelService : IRecipeExcelService +{ + private readonly AppDbContext _db; + private readonly ILogger _logger; + + public RecipeExcelService(AppDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + + public byte[] GenerateTemplate() + { + using var workbook = new XLWorkbook(); + + var recipes = workbook.Worksheets.Add("Recipes"); + recipes.Cell(1, 1).Value = "Name"; + recipes.Cell(1, 2).Value = "MealCategory"; + recipes.Cell(1, 3).Value = "Calories"; + recipes.Cell(1, 4).Value = "Protein"; + recipes.Cell(1, 5).Value = "Fat"; + recipes.Cell(1, 6).Value = "Carbs"; + recipes.Cell(1, 7).Value = "PrepTimeMinutes"; + recipes.Row(1).Style.Font.Bold = true; + recipes.Cell(2, 1).Value = "Oatmeal with berries"; + recipes.Cell(2, 2).Value = "Breakfast"; + recipes.Cell(2, 3).Value = 320; + recipes.Cell(2, 4).Value = 12; + recipes.Cell(2, 5).Value = 8; + recipes.Cell(2, 6).Value = 45; + recipes.Cell(2, 7).Value = 10; + recipes.Columns().AdjustToContents(); + + var ingredients = workbook.Worksheets.Add("Ingredients"); + ingredients.Cell(1, 1).Value = "RecipeName"; + ingredients.Cell(1, 2).Value = "Name"; + ingredients.Cell(1, 3).Value = "AmountGrams"; + ingredients.Cell(1, 4).Value = "Unit"; + ingredients.Cell(1, 5).Value = "SortOrder"; + ingredients.Row(1).Style.Font.Bold = true; + ingredients.Cell(2, 1).Value = "Oatmeal with berries"; + ingredients.Cell(2, 2).Value = "Rolled oats"; + ingredients.Cell(2, 3).Value = 80; + ingredients.Cell(2, 4).Value = "g"; + ingredients.Cell(2, 5).Value = 0; + ingredients.Cell(3, 1).Value = "Oatmeal with berries"; + ingredients.Cell(3, 2).Value = "Blueberries"; + ingredients.Cell(3, 3).Value = 100; + ingredients.Cell(3, 4).Value = "g"; + ingredients.Cell(3, 5).Value = 1; + ingredients.Columns().AdjustToContents(); + + var steps = workbook.Worksheets.Add("Steps"); + steps.Cell(1, 1).Value = "RecipeName"; + steps.Cell(1, 2).Value = "StepNumber"; + steps.Cell(1, 3).Value = "Description"; + steps.Row(1).Style.Font.Bold = true; + steps.Cell(2, 1).Value = "Oatmeal with berries"; + steps.Cell(2, 2).Value = 1; + steps.Cell(2, 3).Value = "Cook oats in milk for 5 minutes."; + steps.Cell(3, 1).Value = "Oatmeal with berries"; + steps.Cell(3, 2).Value = 2; + steps.Cell(3, 3).Value = "Top with berries and serve."; + steps.Columns().AdjustToContents(); + + var help = workbook.Worksheets.Add("Help"); + help.Cell(1, 1).Value = "MealCategory values"; + help.Cell(2, 1).Value = "Breakfast, SecondBreakfast (or Second Breakfast), Lunch, Dinner"; + help.Cell(2, 2).Value = "Or numeric: 0, 1, 2, 3"; + help.Cell(4, 1).Value = "RecipeName in Ingredients/Steps must match Name in Recipes sheet exactly."; + help.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return stream.ToArray(); + } + + public async Task ImportAsync(Stream fileStream, CancellationToken ct = default) + { + var result = new RecipeImportResultDto(); + + using var workbook = new XLWorkbook(fileStream); + var recipeSheet = workbook.Worksheets.FirstOrDefault(w => + w.Name.Equals("Recipes", StringComparison.OrdinalIgnoreCase)); + if (recipeSheet is null) + { + result.Errors.Add("Worksheet 'Recipes' not found."); + return result; + } + + var ingredientSheet = workbook.Worksheets.FirstOrDefault(w => + w.Name.Equals("Ingredients", StringComparison.OrdinalIgnoreCase)); + var stepSheet = workbook.Worksheets.FirstOrDefault(w => + w.Name.Equals("Steps", StringComparison.OrdinalIgnoreCase)); + + var ingredientRows = ParseIngredients(ingredientSheet, result.Errors); + var stepRows = ParseSteps(stepSheet, result.Errors); + + var recipeRows = recipeSheet.RowsUsed().Skip(1); + foreach (var row in recipeRows) + { + var name = row.Cell(1).GetString().Trim(); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + if (!TryParseCategory(row.Cell(2).GetString(), out var category, out var categoryError)) + { + result.Errors.Add($"Recipe '{name}': {categoryError}"); + result.SkippedCount++; + continue; + } + + var dto = new CreateRecipeDto + { + Name = name, + MealCategory = category, + Calories = ParseNullableInt(row.Cell(3)), + Protein = ParseNullableDecimal(row.Cell(4)), + Fat = ParseNullableDecimal(row.Cell(5)), + Carbs = ParseNullableDecimal(row.Cell(6)), + PrepTimeMinutes = ParseNullableInt(row.Cell(7)), + Ingredients = ingredientRows + .Where(i => i.RecipeName.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Select(i => new CreateIngredientDto + { + Name = i.Name, + AmountGrams = i.AmountGrams, + Unit = i.Unit, + SortOrder = i.SortOrder, + }) + .ToList(), + Steps = stepRows + .Where(s => s.RecipeName.Equals(name, StringComparison.OrdinalIgnoreCase)) + .OrderBy(s => s.StepNumber) + .Select(s => new CreateRecipeStepDto + { + StepNumber = s.StepNumber, + Description = s.Description, + }) + .ToList(), + }; + + try + { + var exists = await _db.Recipes.AnyAsync( + r => r.IsActive && r.Name.ToLower() == name.ToLower(), ct); + if (exists) + { + result.Errors.Add($"Recipe '{name}' already exists — skipped."); + result.SkippedCount++; + continue; + } + + var entity = RecipeManagementService.MapToEntity(dto); + entity.CreatedAt = DateTime.UtcNow; + entity.IsActive = true; + _db.Recipes.Add(entity); + await _db.SaveChangesAsync(ct); + result.CreatedCount++; + result.CreatedRecipeIds.Add(entity.Id); + _logger.LogInformation("Imported recipe {RecipeId} '{Name}' from Excel", entity.Id, name); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to import recipe '{Name}'", name); + result.Errors.Add($"Recipe '{name}': {ex.Message}"); + result.SkippedCount++; + } + } + + return result; + } + + private static List ParseIngredients(IXLWorksheet? sheet, List errors) + { + var rows = new List(); + if (sheet is null) + { + return rows; + } + + foreach (var row in sheet.RowsUsed().Skip(1)) + { + var recipeName = row.Cell(1).GetString().Trim(); + var name = row.Cell(2).GetString().Trim(); + if (string.IsNullOrEmpty(recipeName) || string.IsNullOrEmpty(name)) + { + continue; + } + + rows.Add(new IngredientRow( + recipeName, + name, + ParseNullableDecimal(row.Cell(3)), + NullIfEmpty(row.Cell(4).GetString()), + ParseNullableInt(row.Cell(5)) ?? rows.Count(r => + r.RecipeName.Equals(recipeName, StringComparison.OrdinalIgnoreCase)))); + } + + return rows; + } + + private static List ParseSteps(IXLWorksheet? sheet, List errors) + { + var rows = new List(); + if (sheet is null) + { + return rows; + } + + foreach (var row in sheet.RowsUsed().Skip(1)) + { + var recipeName = row.Cell(1).GetString().Trim(); + var stepNumber = ParseNullableInt(row.Cell(2)); + var description = row.Cell(3).GetString().Trim(); + if (string.IsNullOrEmpty(recipeName) || stepNumber is null or < 1 || string.IsNullOrEmpty(description)) + { + continue; + } + + rows.Add(new StepRow(recipeName, stepNumber.Value, description)); + } + + return rows; + } + + internal static bool TryParseCategory(string raw, out int category, out string error) + { + error = string.Empty; + category = 0; + var value = raw.Trim(); + + if (int.TryParse(value, out category) && category is >= 0 and <= 3) + { + return true; + } + + category = value.ToLowerInvariant().Replace(" ", "") switch + { + "breakfast" => 0, + "secondbreakfast" => 1, + "lunch" => 2, + "dinner" => 3, + _ => -1, + }; + + if (category < 0) + { + error = $"Invalid MealCategory '{raw}'. Use Breakfast, SecondBreakfast, Lunch, Dinner or 0–3."; + return false; + } + + return true; + } + + private static int? ParseNullableInt(IXLCell cell) + { + if (cell.IsEmpty()) + { + return null; + } + + if (cell.TryGetValue(out int intValue)) + { + return intValue; + } + + return int.TryParse(cell.GetString(), out intValue) ? intValue : null; + } + + private static decimal? ParseNullableDecimal(IXLCell cell) + { + if (cell.IsEmpty()) + { + return null; + } + + if (cell.TryGetValue(out double doubleValue)) + { + return (decimal)doubleValue; + } + + return decimal.TryParse(cell.GetString(), out var decimalValue) ? decimalValue : null; + } + + private static string? NullIfEmpty(string value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private sealed record IngredientRow(string RecipeName, string Name, decimal? AmountGrams, string? Unit, int SortOrder); + + private sealed record StepRow(string RecipeName, int StepNumber, string Description); +} diff --git a/MealPlan.Api/Services/RecipeManagementService.cs b/MealPlan.Api/Services/RecipeManagementService.cs new file mode 100644 index 0000000..35f13b0 --- /dev/null +++ b/MealPlan.Api/Services/RecipeManagementService.cs @@ -0,0 +1,190 @@ +using MealPlan.Api.Data; +using MealPlan.Api.DTOs.Recipe; +using MealPlan.Api.Models; +using Microsoft.EntityFrameworkCore; + +namespace MealPlan.Api.Services; + +/// Creates and updates recipes in the existing PostgreSQL schema. +public class RecipeManagementService : IRecipeManagementService +{ + private readonly AppDbContext _db; + private readonly IRecipeService _recipeService; + + public RecipeManagementService(AppDbContext db, IRecipeService recipeService) + { + _db = db; + _recipeService = recipeService; + } + + public async Task CreateRecipeAsync(CreateRecipeDto dto, CancellationToken ct = default) + { + var recipe = MapToEntity(dto); + recipe.CreatedAt = DateTime.UtcNow; + recipe.IsActive = true; + + await ApplyMacrosFromCatalogAsync(recipe, ct); + + _db.Recipes.Add(recipe); + await _db.SaveChangesAsync(ct); + + return (await _recipeService.GetRecipeByIdAsync(recipe.Id, ct))!; + } + + public async Task UpdateRecipeAsync(int id, CreateRecipeDto dto, CancellationToken ct = default) + { + var recipe = await _db.Recipes + .Include(r => r.Ingredients) + .Include(r => r.Steps) + .FirstOrDefaultAsync(r => r.Id == id && r.IsActive, ct); + + if (recipe is null) + { + return null; + } + + recipe.Name = dto.Name.Trim(); + recipe.MealCategory = dto.MealCategory; + recipe.Calories = dto.Calories; + recipe.Protein = dto.Protein; + recipe.Fat = dto.Fat; + recipe.Carbs = dto.Carbs; + recipe.PrepTimeMinutes = dto.PrepTimeMinutes; + recipe.UpdatedAt = DateTime.UtcNow; + + _db.Ingredients.RemoveRange(recipe.Ingredients); + _db.RecipeSteps.RemoveRange(recipe.Steps); + + recipe.Ingredients = MapIngredients(dto.Ingredients); + recipe.Steps = MapSteps(dto.Steps); + + await ApplyMacrosFromCatalogAsync(recipe, ct); + + await _db.SaveChangesAsync(ct); + return await _recipeService.GetRecipeByIdAsync(id, ct); + } + + public async Task RecalculateAllMacrosFromCatalogAsync(CancellationToken ct = default) + { + var catalog = await _db.IngredientNutritionCatalog.AsNoTracking() + .Where(i => i.IsActive) + .ToDictionaryAsync(i => i.Id, ct); + + var recipes = await _db.Recipes + .Include(r => r.Ingredients) + .Where(r => r.IsActive) + .ToListAsync(ct); + + var updated = 0; + foreach (var recipe in recipes) + { + if (TrySetMacrosFromCatalog(recipe, catalog, overwrite: true)) + { + recipe.UpdatedAt = DateTime.UtcNow; + updated++; + } + } + + await _db.SaveChangesAsync(ct); + + var unlinked = await _db.Ingredients.CountAsync(i => + i.AmountGrams > 0 && + i.CatalogItemId == null && + (i.Unit == null || !HasNonMacroUnit(i.Unit)), ct); + + return new RecipeMacroRecalcResultDto + { + RecipesProcessed = recipes.Count, + RecipesUpdated = updated, + UnlinkedIngredientRows = unlinked, + }; + } + + internal static Recipe MapToEntity(CreateRecipeDto dto) => + new() + { + Name = dto.Name.Trim(), + MealCategory = dto.MealCategory, + Calories = dto.Calories, + Protein = dto.Protein, + Fat = dto.Fat, + Carbs = dto.Carbs, + PrepTimeMinutes = dto.PrepTimeMinutes, + Ingredients = MapIngredients(dto.Ingredients), + Steps = MapSteps(dto.Steps), + }; + + private static List MapIngredients(IEnumerable items) => + items.Select((item, index) => new Ingredient + { + Name = item.Name.Trim(), + AmountGrams = item.AmountGrams, + Unit = string.IsNullOrWhiteSpace(item.Unit) ? null : item.Unit.Trim(), + SortOrder = item.SortOrder > 0 ? item.SortOrder : index, + CatalogItemId = item.CatalogItemId, + }).ToList(); + + private async Task ApplyMacrosFromCatalogAsync(Recipe recipe, CancellationToken ct) + { + var catalogIds = recipe.Ingredients + .Where(i => i.CatalogItemId.HasValue && i.AmountGrams is > 0 && !HasNonMacroUnit(i.Unit)) + .Select(i => i.CatalogItemId!.Value) + .Distinct() + .ToList(); + + if (catalogIds.Count == 0) return; + + var catalog = await _db.IngredientNutritionCatalog.AsNoTracking() + .Where(i => catalogIds.Contains(i.Id) && i.IsActive) + .ToDictionaryAsync(i => i.Id, ct); + + TrySetMacrosFromCatalog(recipe, catalog, overwrite: false); + } + + private static bool TrySetMacrosFromCatalog( + Recipe recipe, + IReadOnlyDictionary catalog, + bool overwrite) + { + int totalCalories = 0; + decimal totalProtein = 0; + decimal totalFat = 0; + decimal totalCarbs = 0; + var hasAny = false; + + foreach (var ing in recipe.Ingredients) + { + if (ing.CatalogItemId is null || ing.AmountGrams is not > 0 || HasNonMacroUnit(ing.Unit)) continue; + if (!catalog.TryGetValue(ing.CatalogItemId.Value, out var cat)) continue; + + var scale = ing.AmountGrams.Value / 100m; + totalCalories += (int)Math.Round(cat.CaloriesPer100G * scale); + totalProtein += cat.ProteinGPer100G * scale; + totalFat += cat.FatGPer100G * scale; + totalCarbs += cat.CarbsGPer100G * scale; + hasAny = true; + } + + if (!hasAny) return false; + + if (overwrite || !recipe.Calories.HasValue) recipe.Calories = totalCalories; + if (overwrite || !recipe.Protein.HasValue) recipe.Protein = Math.Round(totalProtein, 2); + if (overwrite || !recipe.Fat.HasValue) recipe.Fat = Math.Round(totalFat, 2); + if (overwrite || !recipe.Carbs.HasValue) recipe.Carbs = Math.Round(totalCarbs, 2); + return true; + } + + private static bool HasNonMacroUnit(string? unit) + { + if (string.IsNullOrWhiteSpace(unit)) return false; + var u = unit.Trim().ToLowerInvariant(); + return u is "do smaku" or "optional" or "opcjonalnie" or "szczypta"; + } + + private static List MapSteps(IEnumerable items) => + items.Select(item => new RecipeStep + { + StepNumber = item.StepNumber, + Description = item.Description.Trim(), + }).ToList(); +} diff --git a/MealPlan.Api/Services/RecipeService.cs b/MealPlan.Api/Services/RecipeService.cs index 2a967d9..61ecd51 100644 --- a/MealPlan.Api/Services/RecipeService.cs +++ b/MealPlan.Api/Services/RecipeService.cs @@ -16,7 +16,7 @@ public class RecipeService : IRecipeService public RecipeService(AppDbContext db) => _db = db; public async Task> GetRecipesAsync( - int? category, string? search, CancellationToken ct = default) + int? category, string? search, string? ingredient, CancellationToken ct = default) { var query = _db.Recipes.AsNoTracking().Where(r => r.IsActive); @@ -31,6 +31,13 @@ public class RecipeService : IRecipeService query = query.Where(r => EF.Functions.ILike(r.Name, $"%{term}%")); } + if (!string.IsNullOrWhiteSpace(ingredient)) + { + var ingredientTerm = ingredient.Trim(); + query = query.Where(r => + r.Ingredients.Any(i => EF.Functions.ILike(i.Name, $"%{ingredientTerm}%"))); + } + return await query .OrderBy(r => r.Name) .Select(r => new RecipeListItemDto @@ -69,6 +76,7 @@ public class RecipeService : IRecipeService AmountGrams = i.AmountGrams, Unit = i.Unit, SortOrder = i.SortOrder, + CatalogItemId = i.CatalogItemId, }) .ToList(), Steps = r.Steps diff --git a/MealPlan.Api/Validators/CreateRecipeValidator.cs b/MealPlan.Api/Validators/CreateRecipeValidator.cs new file mode 100644 index 0000000..1bd5130 --- /dev/null +++ b/MealPlan.Api/Validators/CreateRecipeValidator.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using MealPlan.Api.DTOs.Recipe; + +namespace MealPlan.Api.Validators; + +public class CreateRecipeValidator : AbstractValidator +{ + public CreateRecipeValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(255); + RuleFor(x => x.MealCategory).InclusiveBetween(0, 3); + RuleFor(x => x.Calories).GreaterThanOrEqualTo(0).When(x => x.Calories.HasValue); + RuleFor(x => x.PrepTimeMinutes).GreaterThanOrEqualTo(0).When(x => x.PrepTimeMinutes.HasValue); + RuleForEach(x => x.Ingredients).SetValidator(new CreateIngredientValidator()); + RuleForEach(x => x.Steps).SetValidator(new CreateRecipeStepValidator()); + } +} + +public class CreateIngredientValidator : AbstractValidator +{ + public CreateIngredientValidator() + { + RuleFor(x => x.Name).NotEmpty().MaximumLength(255); + RuleFor(x => x.Unit).MaximumLength(50).When(x => x.Unit != null); + RuleFor(x => x.AmountGrams).GreaterThanOrEqualTo(0).When(x => x.AmountGrams.HasValue); + } +} + +public class CreateRecipeStepValidator : AbstractValidator +{ + public CreateRecipeStepValidator() + { + RuleFor(x => x.StepNumber).GreaterThan(0); + RuleFor(x => x.Description).NotEmpty(); + } +} diff --git a/MealPlan.Api/Validators/DietValidators.cs b/MealPlan.Api/Validators/DietValidators.cs new file mode 100644 index 0000000..c4f4ff5 --- /dev/null +++ b/MealPlan.Api/Validators/DietValidators.cs @@ -0,0 +1,64 @@ +using FluentValidation; +using MealPlan.Api.DTOs.Diet; + +namespace MealPlan.Api.Validators; + +public class UserDailyGoalsValidator : AbstractValidator +{ + public UserDailyGoalsValidator() + { + RuleFor(x => x.CalorieGoal).GreaterThanOrEqualTo(0).When(x => x.CalorieGoal.HasValue); + RuleFor(x => x.ProteinGoalG).GreaterThanOrEqualTo(0).When(x => x.ProteinGoalG.HasValue); + RuleFor(x => x.FatGoalG).GreaterThanOrEqualTo(0).When(x => x.FatGoalG.HasValue); + RuleFor(x => x.CarbsGoalG).GreaterThanOrEqualTo(0).When(x => x.CarbsGoalG.HasValue); + RuleFor(x => x.WaterGoalMl).GreaterThan(0).When(x => x.WaterGoalMl.HasValue); + } +} + +public class LogMealValidator : AbstractValidator +{ + public LogMealValidator() + { + RuleFor(x => x.MealCategory).InclusiveBetween(0, 4); + RuleFor(x => x.Portions).GreaterThan(0); + RuleFor(x => x.Name).MaximumLength(255).When(x => x.Name != null); + RuleFor(x => x) + .Must(x => x.RecipeId.HasValue || (x.CatalogItemId.HasValue && x.Grams is > 0) || !string.IsNullOrWhiteSpace(x.Name)) + .WithMessage("Provide recipeId, catalogItemId with grams, or name."); + RuleFor(x => x.Grams).GreaterThan(0).When(x => x.CatalogItemId.HasValue); + RuleFor(x => x.Calories).GreaterThanOrEqualTo(0).When(x => x.Calories.HasValue); + } +} + +public class LogDrinkValidator : AbstractValidator +{ + public LogDrinkValidator() + { + RuleFor(x => x.VolumeMl).GreaterThan(0); + RuleFor(x => x.Name).MaximumLength(255).When(x => x.Name != null); + RuleFor(x => x) + .Must(x => x.DrinkCatalogId.HasValue || !string.IsNullOrWhiteSpace(x.Name)) + .WithMessage("Provide either drinkCatalogId or name."); + RuleFor(x => x.Calories).GreaterThanOrEqualTo(0).When(x => x.Calories.HasValue); + } +} + +public class CreateDietReminderValidator : AbstractValidator +{ + public CreateDietReminderValidator() + { + RuleFor(x => x.ReminderType).InclusiveBetween(0, 3); + RuleFor(x => x.Title).NotEmpty().MaximumLength(120); + RuleFor(x => x.Message).MaximumLength(500).When(x => x.Message != null); + RuleFor(x => x.DaysOfWeekMask).InclusiveBetween(1, 127); + RuleFor(x => x.MealCategory).InclusiveBetween(0, 4).When(x => x.MealCategory.HasValue); + } +} + +public class UpdateDietReminderValidator : AbstractValidator +{ + public UpdateDietReminderValidator() + { + Include(new CreateDietReminderValidator()); + } +} diff --git a/MealPlan.Api/Validators/IngredientCatalogValidators.cs b/MealPlan.Api/Validators/IngredientCatalogValidators.cs new file mode 100644 index 0000000..1071dd1 --- /dev/null +++ b/MealPlan.Api/Validators/IngredientCatalogValidators.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using MealPlan.Api.DTOs.IngredientCatalog; + +namespace MealPlan.Api.Validators; + +public class UpsertIngredientNutritionItemValidator : AbstractValidator +{ + public UpsertIngredientNutritionItemValidator() + { + RuleFor(x => x.CategoryId).GreaterThan(0); + RuleFor(x => x.Name).NotEmpty().MaximumLength(255); + RuleFor(x => x.NamePl).MaximumLength(255).When(x => x.NamePl != null); + RuleFor(x => x.CaloriesPer100G).GreaterThanOrEqualTo(0); + RuleFor(x => x.ProteinGPer100G).GreaterThanOrEqualTo(0); + RuleFor(x => x.FatGPer100G).GreaterThanOrEqualTo(0); + RuleFor(x => x.CarbsGPer100G).GreaterThanOrEqualTo(0); + RuleFor(x => x.FiberGPer100G).GreaterThanOrEqualTo(0).When(x => x.FiberGPer100G.HasValue); + } +} diff --git a/MealPlan.Api/appsettings.Local.json.example b/MealPlan.Api/appsettings.Local.json.example index 961a076..25a16a7 100644 --- a/MealPlan.Api/appsettings.Local.json.example +++ b/MealPlan.Api/appsettings.Local.json.example @@ -1,5 +1,13 @@ { "ConnectionStrings": { "DefaultConnection": "Host=your-db-host;Port=5432;Database=mealplan;Username=user;Password=secret" + }, + "OpenAI": { + "ApiKey": "sk-your-openai-key", + "Model": "gpt-4.1-mini", + "MacrosModel": "gpt-4.1-mini", + "PlanModel": "gpt-4.1-mini", + "RecipeModel": "gpt-4.1", + "MaxTokens": 4096 } } diff --git a/MealPlan.Api/appsettings.json b/MealPlan.Api/appsettings.json index 8481d9b..2ed72a3 100644 --- a/MealPlan.Api/appsettings.json +++ b/MealPlan.Api/appsettings.json @@ -21,5 +21,13 @@ } } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "OpenAI": { + "ApiKey": "", + "Model": "gpt-4.1-mini", + "MacrosModel": "gpt-4.1-mini", + "PlanModel": "gpt-4.1-mini", + "RecipeModel": "gpt-4.1", + "MaxTokens": 4096 + } } diff --git a/README.md b/README.md index 06e64dd..3a2db58 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,22 @@ compatible with hashes produced by ASP.NET Core Identity. ```bash cd DailyMeals # repository root (where DailyMeals.sln lives) ./scripts/install-node.sh + ``` + + **Angular frontend (recommended rewrite):** + + ```bash + cd meal-plan-frontend-angular + npm install + npm start # http://localhost:4200, proxies /api → localhost:5000 + ``` + + **Or React frontend:** + + ```bash cd meal-plan-frontend ../scripts/npm install + ../scripts/npm run dev # http://localhost:5173 ``` 4. In Rider, open the run configuration dropdown (top toolbar). You should see: @@ -155,9 +169,13 @@ All `/api/recipes` endpoints require a valid `Authorization: Bearer = 0), + "ProteinGoalG" numeric(6,2) NULL CHECK ("ProteinGoalG" IS NULL OR "ProteinGoalG" >= 0), + "FatGoalG" numeric(6,2) NULL CHECK ("FatGoalG" IS NULL OR "FatGoalG" >= 0), + "CarbsGoalG" numeric(6,2) NULL CHECK ("CarbsGoalG" IS NULL OR "CarbsGoalG" >= 0), + "WaterGoalMl" int NULL CHECK ("WaterGoalMl" IS NULL OR "WaterGoalMl" > 0), + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL +); + +COMMENT ON TABLE diet."UserDailyGoals" IS 'Per-user daily calorie, macro, and hydration targets (Lifesum-style goals).'; + +-- --------------------------------------------------------------------------- +-- Drink catalog (reference data; quick-add presets like WaterMinder) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DrinkCatalog" ( + "Id" serial PRIMARY KEY, + "Code" varchar(32) NOT NULL UNIQUE, + "Name" varchar(100) NOT NULL, + "DefaultVolumeMl" int NOT NULL CHECK ("DefaultVolumeMl" > 0), + "CaloriesPer100Ml" numeric(6,2) NULL CHECK ("CaloriesPer100Ml" IS NULL OR "CaloriesPer100Ml" >= 0), + "IconEmoji" varchar(16) NULL, + "SortOrder" int NOT NULL DEFAULT 0, + "IsActive" boolean NOT NULL DEFAULT true +); + +COMMENT ON TABLE diet."DrinkCatalog" IS 'Preset drinks for one-tap logging (water, tea, coffee, etc.).'; + +INSERT INTO diet."DrinkCatalog" ("Code", "Name", "DefaultVolumeMl", "CaloriesPer100Ml", "IconEmoji", "SortOrder") +VALUES + ('water', 'Water', 250, 0, '💧', 1), + ('tea', 'Tea', 200, 1, '🍵', 2), + ('coffee', 'Coffee', 150, 2, '☕', 3), + ('juice', 'Juice', 200, 45, '🧃', 4), + ('milk', 'Milk', 250, 42, '🥛', 5), + ('soda', 'Soft drink', 330, 42, '🥤', 6), + ('other', 'Other drink', 250, NULL, '🍶', 99) +ON CONFLICT ("Code") DO NOTHING; + +-- --------------------------------------------------------------------------- +-- Logged meals (MyFitnessPal-style diary entries; optional recipe link) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."MealConsumptions" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL + REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "RecipeId" int NULL + REFERENCES diet."Recipes"("Id") ON DELETE SET NULL, + "MealCategory" int NOT NULL CHECK ("MealCategory" BETWEEN 0 AND 4), + "LogDate" date NOT NULL, + "ConsumedAt" timestamptz NOT NULL, + "Name" varchar(255) NOT NULL, + "Portions" numeric(4,2) NOT NULL DEFAULT 1 CHECK ("Portions" > 0), + "Calories" int NULL CHECK ("Calories" IS NULL OR "Calories" >= 0), + "ProteinG" numeric(6,2) NULL CHECK ("ProteinG" IS NULL OR "ProteinG" >= 0), + "FatG" numeric(6,2) NULL CHECK ("FatG" IS NULL OR "FatG" >= 0), + "CarbsG" numeric(6,2) NULL CHECK ("CarbsG" IS NULL OR "CarbsG" >= 0), + "Notes" text NULL, + "CreatedAt" timestamptz NOT NULL DEFAULT now() +); + +COMMENT ON TABLE diet."MealConsumptions" IS 'Meals the user ate; macros snapshotted at log time.'; +COMMENT ON COLUMN diet."MealConsumptions"."MealCategory" IS '0=Breakfast, 1=SecondBreakfast, 2=Lunch, 3=Dinner, 4=Snack'; + +CREATE INDEX IF NOT EXISTS "IX_MealConsumptions_UserId_LogDate" + ON diet."MealConsumptions" ("UserId", "LogDate" DESC); + +CREATE INDEX IF NOT EXISTS "IX_MealConsumptions_UserId_ConsumedAt" + ON diet."MealConsumptions" ("UserId", "ConsumedAt" DESC); + +-- --------------------------------------------------------------------------- +-- Logged drinks (water tracker entries) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DrinkConsumptions" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL + REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "DrinkCatalogId" int NULL + REFERENCES diet."DrinkCatalog"("Id") ON DELETE SET NULL, + "LogDate" date NOT NULL, + "ConsumedAt" timestamptz NOT NULL, + "Name" varchar(255) NOT NULL, + "VolumeMl" int NOT NULL CHECK ("VolumeMl" > 0), + "Calories" int NULL CHECK ("Calories" IS NULL OR "Calories" >= 0), + "CreatedAt" timestamptz NOT NULL DEFAULT now() +); + +COMMENT ON TABLE diet."DrinkConsumptions" IS 'Hydration and beverage intake log.'; + +CREATE INDEX IF NOT EXISTS "IX_DrinkConsumptions_UserId_LogDate" + ON diet."DrinkConsumptions" ("UserId", "LogDate" DESC); + +-- --------------------------------------------------------------------------- +-- Reminders (server-stored schedules; clients handle local notifications) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DietReminders" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL + REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "ReminderType" int NOT NULL CHECK ("ReminderType" BETWEEN 0 AND 3), + "Title" varchar(120) NOT NULL, + "Message" varchar(500) NULL, + "TimeOfDay" time NOT NULL, + "DaysOfWeekMask" int NOT NULL DEFAULT 127 CHECK ("DaysOfWeekMask" BETWEEN 1 AND 127), + "MealCategory" int NULL CHECK ("MealCategory" IS NULL OR "MealCategory" BETWEEN 0 AND 4), + "IsEnabled" boolean NOT NULL DEFAULT true, + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL +); + +COMMENT ON TABLE diet."DietReminders" IS 'Meal/water/custom reminder schedules (bitmask: Sun=1, Mon=2, Tue=4, Wed=8, Thu=16, Fri=32, Sat=64).'; +COMMENT ON COLUMN diet."DietReminders"."ReminderType" IS '0=Water, 1=Meal, 2=Snack, 3=Custom'; + +CREATE INDEX IF NOT EXISTS "IX_DietReminders_UserId_IsEnabled" + ON diet."DietReminders" ("UserId", "IsEnabled"); + +-- --------------------------------------------------------------------------- +-- Optional views for reporting (API computes the same in LINQ) +-- --------------------------------------------------------------------------- +CREATE OR REPLACE VIEW diet."v_DailyMealTotals" AS +SELECT + "UserId", + "LogDate", + COALESCE(SUM("Calories"), 0)::int AS "TotalCalories", + COALESCE(SUM("ProteinG"), 0) AS "TotalProteinG", + COALESCE(SUM("FatG"), 0) AS "TotalFatG", + COALESCE(SUM("CarbsG"), 0) AS "TotalCarbsG", + COUNT(*)::int AS "MealCount" +FROM diet."MealConsumptions" +GROUP BY "UserId", "LogDate"; + +CREATE OR REPLACE VIEW diet."v_DailyDrinkTotals" AS +SELECT + "UserId", + "LogDate", + COALESCE(SUM("VolumeMl"), 0)::int AS "TotalWaterMl", + COALESCE(SUM("Calories"), 0)::int AS "TotalDrinkCalories", + COUNT(*)::int AS "DrinkCount" +FROM diet."DrinkConsumptions" +GROUP BY "UserId", "LogDate"; + +COMMIT; diff --git a/database/scripts/002_ingredient_nutrition_catalog.sql b/database/scripts/002_ingredient_nutrition_catalog.sql new file mode 100644 index 0000000..a755436 --- /dev/null +++ b/database/scripts/002_ingredient_nutrition_catalog.sql @@ -0,0 +1,144 @@ +-- DailyMeals ingredient nutrition catalog +-- Reference data: macros per 100 g, grouped by food category (meat, vegetables, etc.) +-- Apply manually: psql "$DATABASE_URL" -f database/scripts/002_ingredient_nutrition_catalog.sql + +BEGIN; + +CREATE TABLE IF NOT EXISTS diet."IngredientCategories" ( + "Id" serial PRIMARY KEY, + "Code" varchar(32) NOT NULL UNIQUE, + "Name" varchar(100) NOT NULL, + "SortOrder" int NOT NULL DEFAULT 0, + "IsActive" boolean NOT NULL DEFAULT true +); + +COMMENT ON TABLE diet."IngredientCategories" IS 'Food groups for the nutrition catalog (meat, vegetables, etc.).'; + +CREATE TABLE IF NOT EXISTS diet."IngredientNutritionCatalog" ( + "Id" serial PRIMARY KEY, + "CategoryId" int NOT NULL + REFERENCES diet."IngredientCategories"("Id") ON DELETE RESTRICT, + "Name" varchar(255) NOT NULL, + "CaloriesPer100G" int NOT NULL CHECK ("CaloriesPer100G" >= 0), + "ProteinGPer100G" numeric(6,2) NOT NULL DEFAULT 0 CHECK ("ProteinGPer100G" >= 0), + "FatGPer100G" numeric(6,2) NOT NULL DEFAULT 0 CHECK ("FatGPer100G" >= 0), + "CarbsGPer100G" numeric(6,2) NOT NULL DEFAULT 0 CHECK ("CarbsGPer100G" >= 0), + "FiberGPer100G" numeric(6,2) NULL CHECK ("FiberGPer100G" IS NULL OR "FiberGPer100G" >= 0), + "SortOrder" int NOT NULL DEFAULT 0, + "IsActive" boolean NOT NULL DEFAULT true, + CONSTRAINT "UQ_IngredientNutritionCatalog_Category_Name" UNIQUE ("CategoryId", "Name") +); + +COMMENT ON TABLE diet."IngredientNutritionCatalog" IS 'Ingredient nutrition values per 100 g (calories, protein, fat, carbs).'; + +CREATE INDEX IF NOT EXISTS "IX_IngredientNutritionCatalog_CategoryId" + ON diet."IngredientNutritionCatalog" ("CategoryId", "SortOrder", "Name"); + +CREATE INDEX IF NOT EXISTS "IX_IngredientNutritionCatalog_Name" + ON diet."IngredientNutritionCatalog" (lower("Name")); + +INSERT INTO diet."IngredientCategories" ("Code", "Name", "SortOrder") +VALUES + ('meat', 'Meat', 1), + ('poultry', 'Poultry', 2), + ('fish', 'Fish', 3), + ('seafood', 'Seafood', 4), + ('vegetables', 'Vegetables', 5), + ('fruits', 'Fruits', 6), + ('dairy', 'Dairy', 7), + ('eggs', 'Eggs', 8), + ('grains', 'Grains', 9), + ('legumes', 'Legumes', 10), + ('nuts', 'Nuts & seeds', 11), + ('oils', 'Oils & fats', 12) +ON CONFLICT ("Code") DO NOTHING; + +-- Helper: insert items per category code +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", v."Name", v."Calories", v."Protein", v."Fat", v."Carbs", v."Fiber", v."SortOrder" +FROM (VALUES + -- meat + ('meat', 'Beef sirloin', 271, 25.80, 18.00, 0.00, NULL, 1), + ('meat', 'Pork loin', 242, 27.30, 14.00, 0.00, NULL, 2), + ('meat', 'Lamb leg', 258, 25.60, 16.50, 0.00, NULL, 3), + ('meat', 'Ground beef (15% fat)', 250, 26.00, 15.00, 0.00, NULL, 4), + ('meat', 'Ham (cooked)', 145, 21.00, 6.00, 1.50, NULL, 5), + -- poultry + ('poultry', 'Chicken breast', 165, 31.00, 3.60, 0.00, NULL, 1), + ('poultry', 'Chicken thigh', 209, 26.00, 10.90, 0.00, NULL, 2), + ('poultry', 'Turkey breast', 135, 30.00, 1.00, 0.00, NULL, 3), + ('poultry', 'Duck breast', 202, 23.50, 11.20, 0.00, NULL, 4), + -- fish + ('fish', 'Salmon', 208, 20.40, 13.40, 0.00, NULL, 1), + ('fish', 'Cod', 82, 17.80, 0.70, 0.00, NULL, 2), + ('fish', 'Tuna (canned in water)', 116, 25.50, 0.80, 0.00, NULL, 3), + ('fish', 'Trout', 148, 20.80, 6.60, 0.00, NULL, 4), + ('fish', 'Mackerel', 205, 18.60, 13.90, 0.00, NULL, 5), + -- seafood + ('seafood', 'Shrimp', 99, 24.00, 0.30, 0.20, NULL, 1), + ('seafood', 'Mussels', 172, 23.80, 4.50, 7.40, NULL, 2), + ('seafood', 'Squid', 92, 15.60, 1.40, 3.10, NULL, 3), + ('seafood', 'Crab', 97, 19.40, 1.50, 0.00, NULL, 4), + -- vegetables + ('vegetables', 'Tomato', 18, 0.90, 0.20, 3.90, 1.20, 1), + ('vegetables', 'Carrot', 41, 0.90, 0.20, 9.60, 2.80, 2), + ('vegetables', 'Broccoli', 34, 2.80, 0.40, 6.60, 2.60, 3), + ('vegetables', 'Spinach', 23, 2.90, 0.40, 3.60, 2.20, 4), + ('vegetables', 'Potato', 77, 2.00, 0.10, 17.50, 2.20, 5), + ('vegetables', 'Cucumber', 15, 0.70, 0.10, 3.60, 0.50, 6), + ('vegetables', 'Bell pepper', 31, 1.00, 0.30, 6.00, 2.10, 7), + ('vegetables', 'Onion', 40, 1.10, 0.10, 9.30, 1.70, 8), + ('vegetables', 'Zucchini', 17, 1.20, 0.30, 3.10, 1.00, 9), + ('vegetables', 'Mushrooms', 22, 3.10, 0.30, 3.30, 1.00, 10), + -- fruits + ('fruits', 'Apple', 52, 0.30, 0.20, 13.80, 2.40, 1), + ('fruits', 'Banana', 89, 1.10, 0.30, 22.80, 2.60, 2), + ('fruits', 'Orange', 47, 0.90, 0.10, 11.80, 2.40, 3), + ('fruits', 'Strawberries', 32, 0.70, 0.30, 7.70, 2.00, 4), + ('fruits', 'Blueberries', 57, 0.70, 0.30, 14.50, 2.40, 5), + ('fruits', 'Grapes', 69, 0.70, 0.20, 18.10, 0.90, 6), + ('fruits', 'Avocado', 160, 2.00, 14.70, 8.50, 6.70, 7), + -- dairy + ('dairy', 'Whole milk', 61, 3.20, 3.30, 4.80, NULL, 1), + ('dairy', 'Skim milk', 34, 3.40, 0.10, 5.00, NULL, 2), + ('dairy', 'Greek yogurt (plain)', 59, 10.00, 0.40, 3.60, NULL, 3), + ('dairy', 'Cheddar cheese', 403, 25.00, 33.00, 1.30, NULL, 4), + ('dairy', 'Mozzarella', 280, 28.00, 17.00, 3.10, NULL, 5), + ('dairy', 'Cottage cheese', 98, 11.10, 4.30, 3.40, NULL, 6), + -- eggs + ('eggs', 'Chicken egg (whole)', 155, 12.60, 10.60, 1.10, NULL, 1), + ('eggs', 'Egg white', 52, 10.90, 0.20, 0.70, NULL, 2), + ('eggs', 'Egg yolk', 322, 15.90, 26.50, 3.60, NULL, 3), + -- grains + ('grains', 'White rice (cooked)', 130, 2.70, 0.30, 28.20, 0.40, 1), + ('grains', 'Brown rice (cooked)', 123, 2.70, 1.00, 25.60, 1.60, 2), + ('grains', 'Pasta (cooked)', 131, 5.00, 1.10, 25.00, 1.80, 3), + ('grains', 'Oats (dry)', 389, 16.90, 6.90, 66.30, 10.60, 4), + ('grains', 'Whole wheat bread', 247, 13.00, 3.40, 41.00, 6.00, 5), + ('grains', 'White bread', 265, 9.00, 3.20, 49.00, 2.70, 6), + ('grains', 'Quinoa (cooked)', 120, 4.40, 1.90, 21.30, 2.80, 7), + -- legumes + ('legumes', 'Lentils (cooked)', 116, 9.00, 0.40, 20.10, 7.90, 1), + ('legumes', 'Chickpeas (cooked)', 164, 8.90, 2.60, 27.40, 7.60, 2), + ('legumes', 'Black beans (cooked)', 132, 8.90, 0.50, 23.70, 8.70, 3), + ('legumes', 'Kidney beans (cooked)', 127, 8.70, 0.50, 22.80, 6.40, 4), + ('legumes', 'Green peas (cooked)', 84, 5.40, 0.20, 15.60, 5.50, 5), + ('legumes', 'Tofu (firm)', 76, 8.10, 4.80, 1.90, 0.30, 6), + -- nuts + ('nuts', 'Almonds', 579, 21.20, 49.90, 21.60, 12.50, 1), + ('nuts', 'Walnuts', 654, 15.20, 65.20, 13.70, 6.70, 2), + ('nuts', 'Peanuts', 567, 25.80, 49.20, 16.10, 8.50, 3), + ('nuts', 'Cashews', 553, 18.20, 43.80, 30.20, 3.30, 4), + ('nuts', 'Sunflower seeds', 584, 20.80, 51.50, 20.00, 8.60, 5), + ('nuts', 'Chia seeds', 486, 16.50, 30.70, 42.10, 34.40, 6), + -- oils + ('oils', 'Olive oil', 884, 0.00, 100.00, 0.00, NULL, 1), + ('oils', 'Butter', 717, 0.90, 81.00, 0.10, NULL, 2), + ('oils', 'Coconut oil', 862, 0.00, 100.00, 0.00, NULL, 3), + ('oils', 'Rapeseed oil', 884, 0.00, 100.00, 0.00, NULL, 4) +) AS v("CategoryCode", "Name", "Calories", "Protein", "Fat", "Carbs", "Fiber", "SortOrder") +JOIN diet."IngredientCategories" c ON c."Code" = v."CategoryCode" +ON CONFLICT ("CategoryId", "Name") DO NOTHING; + +COMMIT; diff --git a/database/scripts/003_ingredient_catalog_polish_names.sql b/database/scripts/003_ingredient_catalog_polish_names.sql new file mode 100644 index 0000000..925cd62 --- /dev/null +++ b/database/scripts/003_ingredient_catalog_polish_names.sql @@ -0,0 +1,83 @@ +-- Add Polish display names to the ingredient nutrition catalog. +-- Safe to re-run. Apply after 002_ingredient_nutrition_catalog.sql: +-- psql "$DATABASE_URL" -f database/scripts/003_ingredient_catalog_polish_names.sql + +BEGIN; + +ALTER TABLE diet."IngredientNutritionCatalog" + ADD COLUMN IF NOT EXISTS "NamePl" varchar(255) NULL; + +COMMENT ON COLUMN diet."IngredientNutritionCatalog"."NamePl" IS 'Polish display name for the ingredient.'; + +CREATE INDEX IF NOT EXISTS "IX_IngredientNutritionCatalog_NamePl" + ON diet."IngredientNutritionCatalog" (lower("NamePl")); + +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Polędwica wołowa' WHERE "Name" = 'Beef sirloin'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Schab wieprzowy' WHERE "Name" = 'Pork loin'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Udziec jagnięcy' WHERE "Name" = 'Lamb leg'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Mięso mielone wołowe (15% tł.)' WHERE "Name" = 'Ground beef (15% fat)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Szynka (gotowana)' WHERE "Name" = 'Ham (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pierś z kurczaka' WHERE "Name" = 'Chicken breast'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Udo z kurczaka' WHERE "Name" = 'Chicken thigh'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pierś z indyka' WHERE "Name" = 'Turkey breast'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pierś z kaczki' WHERE "Name" = 'Duck breast'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Łosoś' WHERE "Name" = 'Salmon'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Dorsz' WHERE "Name" = 'Cod'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Tuńczyk (w wodzie)' WHERE "Name" = 'Tuna (canned in water)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pstrąg' WHERE "Name" = 'Trout'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Makrela' WHERE "Name" = 'Mackerel'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Krewetki' WHERE "Name" = 'Shrimp'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Małże' WHERE "Name" = 'Mussels'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Kałamarnica' WHERE "Name" = 'Squid'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Krab' WHERE "Name" = 'Crab'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pomidor' WHERE "Name" = 'Tomato'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Marchew' WHERE "Name" = 'Carrot'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Brokuły' WHERE "Name" = 'Broccoli'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Szpinak' WHERE "Name" = 'Spinach'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ziemniak' WHERE "Name" = 'Potato'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ogórek' WHERE "Name" = 'Cucumber'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Papryka' WHERE "Name" = 'Bell pepper'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Cebula' WHERE "Name" = 'Onion'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Cukinia' WHERE "Name" = 'Zucchini'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pieczarki' WHERE "Name" = 'Mushrooms'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Jabłko' WHERE "Name" = 'Apple'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Banan' WHERE "Name" = 'Banana'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Pomarańcza' WHERE "Name" = 'Orange'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Truskawki' WHERE "Name" = 'Strawberries'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Borówki' WHERE "Name" = 'Blueberries'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Winogrona' WHERE "Name" = 'Grapes'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Awokado' WHERE "Name" = 'Avocado'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Mleko pełne' WHERE "Name" = 'Whole milk'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Mleko odtłuszczone' WHERE "Name" = 'Skim milk'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Jogurt grecki (naturalny)' WHERE "Name" = 'Greek yogurt (plain)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ser cheddar' WHERE "Name" = 'Cheddar cheese'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Mozzarella' WHERE "Name" = 'Mozzarella'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Twaróg' WHERE "Name" = 'Cottage cheese'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Jajko kurze (całe)' WHERE "Name" = 'Chicken egg (whole)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Białko jaja' WHERE "Name" = 'Egg white'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Żółtko jaja' WHERE "Name" = 'Egg yolk'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ryż biały (gotowany)' WHERE "Name" = 'White rice (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ryż brązowy (gotowany)' WHERE "Name" = 'Brown rice (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Makaron (gotowany)' WHERE "Name" = 'Pasta (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Płatki owsiane (suche)' WHERE "Name" = 'Oats (dry)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Chleb pełnoziarnisty' WHERE "Name" = 'Whole wheat bread'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Chleb biały' WHERE "Name" = 'White bread'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Komosa ryżowa (gotowana)' WHERE "Name" = 'Quinoa (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Soczewica (gotowana)' WHERE "Name" = 'Lentils (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Ciecierzyca (gotowana)' WHERE "Name" = 'Chickpeas (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Czarna fasola (gotowana)' WHERE "Name" = 'Black beans (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Fasola czerwona (gotowana)' WHERE "Name" = 'Kidney beans (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Groszek (gotowany)' WHERE "Name" = 'Green peas (cooked)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Tofu (twarde)' WHERE "Name" = 'Tofu (firm)'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Migdały' WHERE "Name" = 'Almonds'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Orzechy włoskie' WHERE "Name" = 'Walnuts'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Orzeszki ziemne' WHERE "Name" = 'Peanuts'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Orzechy nerkowca' WHERE "Name" = 'Cashews'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Nasiona słonecznika' WHERE "Name" = 'Sunflower seeds'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Nasiona chia' WHERE "Name" = 'Chia seeds'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Oliwa z oliwek' WHERE "Name" = 'Olive oil'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Masło' WHERE "Name" = 'Butter'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Olej kokosowy' WHERE "Name" = 'Coconut oil'; +UPDATE diet."IngredientNutritionCatalog" SET "NamePl" = 'Olej rzepakowy' WHERE "Name" = 'Rapeseed oil'; + +COMMIT; diff --git a/database/scripts/004_extended_features.sql b/database/scripts/004_extended_features.sql new file mode 100644 index 0000000..2976a34 --- /dev/null +++ b/database/scripts/004_extended_features.sql @@ -0,0 +1,62 @@ +-- Extended features: refresh tokens, meal planner, favorites, recipe-catalog link +-- Apply after 001–003. Schema: diet + +BEGIN; + +-- --------------------------------------------------------------------------- +-- Refresh tokens (persist sessions across API restarts) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."RefreshTokens" ( + "Token" varchar(128) PRIMARY KEY, + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "ExpiresAtUtc" timestamptz NOT NULL, + "CreatedAtUtc" timestamptz NOT NULL DEFAULT now(), + "IsRevoked" boolean NOT NULL DEFAULT false +); + +CREATE INDEX IF NOT EXISTS "IX_RefreshTokens_UserId" ON diet."RefreshTokens" ("UserId"); +CREATE INDEX IF NOT EXISTS "IX_RefreshTokens_ExpiresAtUtc" ON diet."RefreshTokens" ("ExpiresAtUtc"); + +-- --------------------------------------------------------------------------- +-- Weekly meal plan (one recipe per meal slot per day) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."MealPlanEntries" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "PlanDate" date NOT NULL, + "MealCategory" int NOT NULL CHECK ("MealCategory" BETWEEN 0 AND 4), + "RecipeId" int NOT NULL REFERENCES diet."Recipes"("Id") ON DELETE CASCADE, + "Portions" numeric(4,2) NOT NULL DEFAULT 1 CHECK ("Portions" > 0), + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL, + UNIQUE ("UserId", "PlanDate", "MealCategory") +); + +CREATE INDEX IF NOT EXISTS "IX_MealPlanEntries_UserId_PlanDate" + ON diet."MealPlanEntries" ("UserId", "PlanDate"); + +-- --------------------------------------------------------------------------- +-- Favorites (quick logging) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."UserFavoriteRecipes" ( + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "RecipeId" int NOT NULL REFERENCES diet."Recipes"("Id") ON DELETE CASCADE, + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY ("UserId", "RecipeId") +); + +CREATE TABLE IF NOT EXISTS diet."UserFavoriteCatalogItems" ( + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "CatalogItemId" int NOT NULL REFERENCES diet."IngredientNutritionCatalog"("Id") ON DELETE CASCADE, + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY ("UserId", "CatalogItemId") +); + +-- --------------------------------------------------------------------------- +-- Link recipe ingredients to nutrition catalog (auto macros) +-- --------------------------------------------------------------------------- +ALTER TABLE diet."Ingredients" + ADD COLUMN IF NOT EXISTS "CatalogItemId" int NULL + REFERENCES diet."IngredientNutritionCatalog"("Id") ON DELETE SET NULL; + +COMMIT; diff --git a/database/scripts/005_catalog_expansion_and_ingredient_linking.sql b/database/scripts/005_catalog_expansion_and_ingredient_linking.sql new file mode 100644 index 0000000..a388624 --- /dev/null +++ b/database/scripts/005_catalog_expansion_and_ingredient_linking.sql @@ -0,0 +1,928 @@ +-- 005_catalog_expansion_and_ingredient_linking.sql +-- Expands nutrition catalog, links recipe ingredients, recalculates recipe macros. +-- Coverage estimate: ~1160/1360 ingredient rows linkable; spice-skip 58; unmapped names 26 (mostly spice mixes) +-- Apply after scripts 001-004. +BEGIN; + +-- 1. Add missing catalog items +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Milk 2%', 'Mleko 2%', 50, 3.4, 2.0, 4.8, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'dairy' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Feta cheese', 'Ser feta', 264, 14.0, 21.0, 4.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'dairy' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Ricotta', 'Ricotta', 174, 11.0, 13.0, 3.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'dairy' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Parmesan', 'Parmezan', 431, 38.0, 29.0, 4.1, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'dairy' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Sour cream 12%', 'Śmietana 12%', 130, 2.7, 12.0, 3.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'dairy' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Rye bread whole grain', 'Chleb żytni razowy', 217, 8.5, 1.8, 42.0, 5.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pasta whole grain dry', 'Makaron pełnoziarnisty (suchy)', 348, 12.0, 2.0, 72.0, 3.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'White rice dry', 'Ryż biały (suchy)', 360, 7.0, 0.6, 79.0, 1.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Brown rice dry', 'Ryż brązowy (suchy)', 362, 7.5, 2.7, 76.0, 3.4, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Buckwheat groats dry', 'Kasza gryczana (sucha)', 343, 13.0, 3.4, 72.0, 10.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Bulgur dry', 'Kasza bulgur (sucha)', 342, 12.0, 1.3, 76.0, 12.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Couscous dry', 'Kuskus (suchy)', 376, 13.0, 0.6, 77.0, 5.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Whole wheat tortilla', 'Tortilla pełnoziarnista', 298, 9.0, 7.0, 48.0, 6.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Wheat flour type 1850', 'Mąka pszenna typ 1850', 340, 10.0, 1.0, 72.0, 2.7, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Breadcrumbs whole grain', 'Bułka tarta pełnoziarnista', 395, 13.0, 5.0, 72.0, 4.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Green beans', 'Fasolka szparagowa', 31, 1.8, 0.2, 7.0, 2.7, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Eggplant', 'Bakłażan', 25, 1.0, 0.2, 6.0, 3.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Radish', 'Rzodkiewka', 16, 0.7, 0.1, 3.4, 1.6, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Lettuce mix', 'Sałata / mix sałat', 15, 1.4, 0.2, 2.9, 1.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Leek', 'Por', 61, 1.5, 0.3, 14.0, 1.8, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Celery root', 'Seler korzeniowy', 42, 1.5, 0.3, 9.0, 1.8, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Celery stalk', 'Seler naciowy', 16, 0.7, 0.2, 3.0, 1.6, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Cauliflower', 'Kalafior', 25, 1.9, 0.3, 5.0, 2.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Parsley fresh', 'Pietruszka / natka', 36, 3.0, 0.8, 6.3, 3.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Chives', 'Szczypiorek', 30, 3.3, 0.7, 4.4, 2.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Dill fresh', 'Koper ogrodowy', 43, 3.5, 1.1, 7.0, 2.1, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Capers', 'Kapary', 23, 2.4, 0.9, 4.9, 3.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Olives black', 'Oliwki czarne', 115, 0.8, 10.7, 6.0, 3.2, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Olives green', 'Oliwki zielone', 145, 1.0, 15.0, 3.8, 3.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Garlic', 'Czosnek', 149, 6.4, 0.5, 33.0, 2.1, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Sweet corn canned', 'Kukurydza konserwowa', 86, 2.5, 1.0, 19.0, 2.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Raspberries', 'Maliny', 52, 1.2, 0.7, 12.0, 6.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Kiwi', 'Kiwi', 61, 1.1, 0.5, 15.0, 3.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Mango', 'Mango', 60, 0.8, 0.4, 15.0, 1.6, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Peach nectarine', 'Brzoskwinia / nektarynka', 44, 0.9, 0.3, 11.0, 1.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Lemon juice', 'Sok cytrynowy / cytryna', 22, 0.4, 0.2, 6.9, 0.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pomegranate seeds', 'Pestki granatu', 83, 1.7, 1.2, 19.0, 4.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Dried apricots', 'Suszone morele', 241, 3.4, 0.5, 63.0, 7.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Dried figs', 'Suszone figi', 249, 3.3, 0.9, 63.9, 9.8, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Dried cranberries', 'Żurawina suszona', 308, 0.1, 1.4, 82.0, 5.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Honey', 'Miód', 304, 0.3, 0.0, 82.0, 0.2, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Tahini', 'Tahini', 595, 17.0, 54.0, 21.0, 9.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Hummus', 'Hummus', 166, 8.0, 9.6, 14.0, 6.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'legumes' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pesto basil', 'Pesto bazyliowe', 420, 5.0, 40.0, 6.0, 2.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'oils' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Tomato passata', 'Passata / pomidory z puszki', 32, 1.5, 0.2, 5.0, 1.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Sun-dried tomatoes in oil', 'Suszone pomidory w oliwie', 258, 14.0, 3.0, 55.0, 12.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Jam strawberry low sugar', 'Dżem truskawkowy niskosłodzony', 180, 0.5, 0.1, 45.0, 1.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Ketchup', 'Keczup', 100, 1.0, 0.2, 25.0, 0.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Mustard dijon', 'Musztarda dijonska', 66, 4.0, 3.7, 5.0, 3.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Soy sauce', 'Sos sojowy', 53, 8.0, 0.0, 4.9, 0.8, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Carrot juice', 'Sok marchwiowy', 40, 0.9, 0.2, 9.6, 0.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fruits' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Flaxseed oil', 'Olej lniany', 884, 0.0, 100.0, 0.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'oils' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Flax seeds ground', 'Siemię lniane', 534, 18.0, 42.0, 29.0, 27.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pumpkin seeds', 'Pestki dyni', 559, 30.0, 49.0, 11.0, 6.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pistachios', 'Pistacje', 562, 20.0, 45.0, 28.0, 10.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pine nuts', 'Piniole', 673, 14.0, 68.0, 13.0, 3.7, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Coconut flakes', 'Wiórki kokosowe', 660, 7.0, 65.0, 24.0, 16.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'nuts' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Smoked salmon', 'Łosoś wędzony', 117, 18.0, 4.3, 0.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'fish' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Chicken liver', 'Wątróbka kurczaka', 167, 26.0, 6.5, 1.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'poultry' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Pork tenderloin', 'Polędwica wieprzowa', 143, 26.0, 3.5, 0.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'meat' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Beef stew meat', 'Wołowina gulaszowa', 250, 26.0, 15.0, 0.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'meat' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Beef strips', 'Wołowina (rostbef)', 271, 25.8, 18.0, 0.0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'meat' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +-- Herbs, spices, and zero-calorie liquids (for remaining unmapped ingredient rows with grams) +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Water', 'Woda', 0, 0, 0, 0, NULL, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Basil fresh', 'Bazylia świeża', 23, 3.2, 0.6, 2.7, 1.6, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Basil dried', 'Bazylia suszona', 251, 14.4, 4.1, 47.8, 37.7, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Oregano dried', 'Oregano suszone', 265, 9.0, 4.3, 68.9, 42.5, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Thyme dried', 'Tymianek suszony', 276, 9.1, 7.4, 63.9, 37.0, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Rosemary dried', 'Rozmaryn suszony', 331, 4.9, 15.2, 64.1, 42.6, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Cinnamon ground', 'Cynamon mielony', 247, 4.0, 1.2, 80.6, 53.1, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Ginger ground', 'Imbir mielony', 335, 9.0, 4.2, 71.6, 14.1, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Coriander dried', 'Kolendra suszona', 298, 12.4, 17.8, 54.9, 41.9, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Marjoram dried', 'Majeranek suszony', 271, 12.7, 7.0, 60.6, 40.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Black pepper ground', 'Pieprz czarny mielony', 251, 10.4, 3.3, 63.9, 25.3, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'vegetables' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +INSERT INTO diet."IngredientNutritionCatalog" + ("CategoryId", "Name", "NamePl", "CaloriesPer100G", "ProteinGPer100G", "FatGPer100G", "CarbsGPer100G", "FiberGPer100G", "SortOrder") +SELECT c."Id", 'Cocoa powder 16% fat', 'Kakao 16% proszek', 228, 19.6, 13.7, 57.9, 33.2, 100 +FROM diet."IngredientCategories" c WHERE c."Code" = 'grains' +ON CONFLICT ("CategoryId", "Name") DO UPDATE SET + "NamePl" = EXCLUDED."NamePl", "CaloriesPer100G" = EXCLUDED."CaloriesPer100G", + "ProteinGPer100G" = EXCLUDED."ProteinGPer100G", "FatGPer100G" = EXCLUDED."FatGPer100G", + "CarbsGPer100G" = EXCLUDED."CarbsGPer100G", "FiberGPer100G" = EXCLUDED."FiberGPer100G", "IsActive" = true; + +CREATE TEMP TABLE _ingredient_name_map (pattern text PRIMARY KEY, catalog_name text NOT NULL); +INSERT INTO _ingredient_name_map VALUES ('Awokado', 'Avocado') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Bakłażan', 'Eggplant') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Bazylia suszona', 'Basil dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Bazylia świeża', 'Basil fresh') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Banan', 'Banana') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Bataty', 'Potato') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Borówki amerykańskie', 'Blueberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Brokuł', 'Broccoli') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Brokuły', 'Broccoli') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Brzoskwinia', 'Peach nectarine') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Brzoskwinia (świeża lub mrożona)', 'Peach nectarine') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Bułka tarta pełnoziarnista', 'Breadcrumbs whole grain') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cebula', 'Onion') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cebula czerwona', 'Onion') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Chleb pita pełnoziarnisty', 'Whole wheat bread') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Chleb żytni razowy', 'Rye bread whole grain') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ciecierzyca z puszki', 'Chickpeas (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ciecierzyca z puszki (odsączona)', 'Chickpeas (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cukinia', 'Zucchini') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cynamon', 'Cinnamon ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cytryna', 'Lemon juice') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Cytryna (sok)', 'Lemon juice') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Czosnek', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Czosnek granulowany', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Czosnek, sól, pieprz', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Czosnek, sół, pieprz', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Czosnek, tymianek, sól, pieprz', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Dorsz świeży filety bez skóry', 'Cod') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Dynia pestki łuskane', 'Pumpkin seeds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Dżem truskawkowy niskosłodzony', 'Strawberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Fasola czerwona w zalewie (konserwowa)', 'Kidney beans (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Fasola szparagowa', 'Green beans') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Fasolka szparagowa', 'Green beans') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Fasolka szparagowa mrożona', 'Green beans') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Filet z dorsza lub dorada', 'Cod') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Filet z piersi indyka', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Filet z piersi indyka (rozbite plastry)', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Filet z łososia', 'Salmon') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Gruszka', 'Peach nectarine') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Hummus', 'Hummus') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Imbir mielony', 'Ginger ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jabłko', 'Apple') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jaja kurze całe', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jaja kurze całe (na twardo)', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jajko', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jajko (do klopsików)', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jajko (do panierki)', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jajko ugotowane na twardo', 'Chicken egg (whole)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jogurt grecki 2%', 'Greek yogurt (plain)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Jogurt naturalny', 'Greek yogurt (plain)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kakao 16% proszek', 'Cocoa powder 16% fat') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kabanosy z kurczaka', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kalafior', 'Cauliflower') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kapary', 'Capers') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kasza bulgur', 'Bulgur dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kasza gryczana', 'Buckwheat groats dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kasza jaglana', 'Buckwheat groats dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Keczup', 'Ketchup') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kiełbasa chorizo', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kiełki brokuła', 'Broccoli') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kiwi', 'Kiwi') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Koncentrat pomidorowy 30%', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kolendra suszone liście', 'Coriander dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Koper ogrodowy', 'Dill fresh') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kukurydza konserwowa', 'Sweet corn canned') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Kuskus pełnoziarnisty', 'Couscous dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Len mielony', 'Flax seeds ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Len nasiona', 'Flax seeds ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Len, nasiona', 'Flax seeds ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron fusilli pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron penne pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron pełnoziarnisty (fusilli)', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron pełnoziarnisty (penne lub spaghetti)', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron pełnoziarnisty (tagliatelle lub penne)', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron rigatoni pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron spaghetti pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Makaron tagliatelle pełnoziarnisty', 'Pasta whole grain dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Majeranek', 'Marjoram dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Maliny', 'Raspberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Maliny (świeże lub mrożone)', 'Raspberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Maliny świeże lub mrożone', 'Raspberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mango (świeże lub mrożone)', 'Mango') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Marchew', 'Carrot') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Marchewka', 'Carrot') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Masło', 'Butter') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Masło ekstra', 'Butter') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Masło orzechowe', 'Butter') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Migdały', 'Almonds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Migdały w płatkach', 'Almonds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mix sałat', 'Lettuce mix') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Miód', 'Honey') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Miód pszczeli', 'Honey') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mięso wołowe (świeżo mielone z rostbefu)', 'Ground beef (15% fat)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mięso wołowe mielone', 'Ground beef (15% fat)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mięso z piersi kurczaka bez skóry', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mięso z podudzia indyka bez skóry', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mleko 2%', 'Milk 2%') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mleko spożywcze 2%', 'Milk 2%') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Morszczuk świeży', 'Cod') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Musztarda dijonska', 'Mustard dijon') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mąka pszenna (do obtoczenia)', 'Wheat flour type 1850') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Mąka pszenna typ 1850', 'Wheat flour type 1850') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Nasiona chia', 'Chia seeds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Natka pietruszki', 'Parsley fresh') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Nektarynka', 'Peach nectarine') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Noga (udo) kurczaka', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ocet jabłkowy z dojrzałych jabłek', 'Apple') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Olej lniany tłoczony na zimno', 'Flaxseed oil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oliwa z oliwek', 'Olive oil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oliwki czarne', 'Olives black') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oliwki czarne i zielone (mix)', 'Olives black') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oliwki zielone', 'Olives green') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oliwki zielone marynowane', 'Olives green') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oregano suszone', 'Oregano dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oregano, sok cytryny, sól, pieprz', 'Lemon juice') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Oregano, tymianek, czosnek granulowany, sól', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Orzechy nerkowca', 'Cashews') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Orzechy nerkowca (bez soli)', 'Cashews') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Orzechy włoskie', 'Walnuts') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Papryka czerwona', 'Bell pepper') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Parmezan tarty', 'Parmesan') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Parmezan w płatkach', 'Parmesan') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Passata pomidorowa', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Passata pomidorowa (przecier)', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Passata pomidorowa przecier', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pestki granatu', 'Pomegranate seeds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pesto bazyliowe', 'Pesto basil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pieczarka uprawna świeża', 'Mushrooms') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pieczarki świeże', 'Mushrooms') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pieprz czarny mielony', 'Black pepper ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pierś z indyka', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pierś z kurczaka', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pierś z kurczaka (rozbita cienko)', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pierś z kurczaka (ugotowana lub z poprzedniego dnia)', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pierś z kurczaka (z poprzedniego dnia lub świeża)', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pietruszka korzeń', 'Celery root') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pietruszka liście', 'Parsley fresh') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pietruszka, liście', 'Parsley fresh') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Piniole', 'Pine nuts') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pistacje (łuskane)', 'Pistachios') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pistacje łuskane', 'Pistachios') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Polędwica sopocka', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Polędwica wieprzowa', 'Pork tenderloin') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Polędwica wieprzowa surowa', 'Pork tenderloin') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomarańcza', 'Orange') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomidor', 'Tomato') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomidory cherry', 'Tomato') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomidory koktajlowe', 'Tomato') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomidory z puszki', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Pomidory z puszki (krojone)', 'Tomato passata') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Por', 'Leek') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Płatki owsiane', 'Oats (dry)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ricotta', 'Ricotta') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Rozmaryn', 'Rosemary dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Rukola', 'Lettuce mix') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ryż basmati', 'White rice dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ryż brązowy', 'Brown rice dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ryż jaśminowy', 'White rice dry') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Rzodkiewka', 'Radish') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Salami', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Salami (cienkie plastry)', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sałata', 'Lettuce mix') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sałata lub mix sałat', 'Lettuce mix') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Seler korzeniowy', 'Celery root') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Seler naciowy', 'Celery stalk') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser feta', 'Feta cheese') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser gouda tłusty', 'Parmesan') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser mozzarella', 'Mozzarella') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser parmezan (tarty)', 'Parmesan') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser parmezan tarty', 'Parmesan') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser twarogowy półtłusty', 'Cottage cheese') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ser typu feta', 'Feta cheese') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Serek wiejski naturalny', 'Greek yogurt (plain)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Siemię lniane mielone', 'Flax seeds ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Siemię lniane świeżo mielone', 'Flax seeds ground') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Soczewica zielona', 'Lentils (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Soczewica zielona (ugotowana)', 'Lentils (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sok cytrynowy', 'Lemon juice') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sok marchwiowy', 'Carrot juice') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sos sojowy', 'Soy sauce') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Sos sojowy ciemny', 'Soy sauce') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Suszone figi', 'Dried figs') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Suszone morele', 'Dried apricots') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Suszone pomidory (odsączone)', 'Sun-dried tomatoes in oil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Suszone pomidory (w oleju z ziołami, odsączone)', 'Sun-dried tomatoes in oil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Suszone pomidory w oliwie (odsączone)', 'Sun-dried tomatoes in oil') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szczypiorek', 'Chives') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szpinak', 'Spinach') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szpinak świeży', 'Spinach') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szynka parmeńska', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szynka parmeńska (Prosciutto)', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szynka parmeńska (prosciutto)', 'Ham (cooked)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szynka z indyka', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Szynka z indyka (bez konserwantów)', 'Turkey breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Słonecznik nasiona łuskane', 'Pumpkin seeds') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tahini', 'Tahini') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tortilla pełnoziarnista', 'Whole wheat tortilla') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Truskawki', 'Strawberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Truskawki (świeże lub mrożone)', 'Strawberries') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tuńczyk w wodzie (odsączony)', 'Tuna (canned in water)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Twaróg półtłusty', 'Cottage cheese') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tymianek', 'Thyme dried') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tymianek, czosnek granulowany, sól', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Tymianek, czosnek granulowany, sól, pieprz', 'Garlic') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Udka kurczaka (bez skóry)', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Udka kurczaka bez skóry', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wiórki kokosowe', 'Coconut flakes') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Woda', 'Water') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wołowina (rostbef lub ligawa, cienkie paski)', 'Beef stew meat') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wołowina gulaszowa', 'Beef stew meat') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wołowina gulaszowa (karkówka/łopatka)', 'Beef stew meat') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wołowina mielona', 'Ground beef (15% fat)') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Wątróbka kurczaka', 'Chicken breast') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Ziemniaki', 'Potato') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Łosoś wędzony', 'Smoked salmon') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Śmietana 12%', 'Sour cream 12%') ON CONFLICT DO NOTHING; +INSERT INTO _ingredient_name_map VALUES ('Żurawina suszona', 'Dried cranberries') ON CONFLICT DO NOTHING; + +UPDATE diet."Ingredients" i +SET "CatalogItemId" = c."Id" +FROM _ingredient_name_map m +JOIN diet."IngredientNutritionCatalog" c ON c."Name" = m.catalog_name AND c."IsActive" = true +WHERE i."Name" = m.pattern AND i."AmountGrams" IS NOT NULL AND i."AmountGrams" > 0; + +WITH totals AS ( + SELECT i."RecipeId", + ROUND(SUM(c."CaloriesPer100G" * i."AmountGrams" / 100.0))::int AS calories, + ROUND(SUM(c."ProteinGPer100G" * i."AmountGrams" / 100.0), 2) AS protein, + ROUND(SUM(c."FatGPer100G" * i."AmountGrams" / 100.0), 2) AS fat, + ROUND(SUM(c."CarbsGPer100G" * i."AmountGrams" / 100.0), 2) AS carbs + FROM diet."Ingredients" i + JOIN diet."IngredientNutritionCatalog" c ON c."Id" = i."CatalogItemId" + WHERE i."AmountGrams" IS NOT NULL AND i."AmountGrams" > 0 + GROUP BY i."RecipeId" +) +UPDATE diet."Recipes" r SET + "Calories" = t.calories, "Protein" = t.protein, "Fat" = t.fat, "Carbs" = t.carbs, "UpdatedAt" = now() +FROM totals t WHERE r."Id" = t."RecipeId" AND r."IsActive" = true; + +DO $$ +DECLARE unlinked int; no_macros int; +BEGIN + SELECT COUNT(*) INTO unlinked FROM diet."Ingredients" + WHERE "AmountGrams" > 0 AND "CatalogItemId" IS NULL AND COALESCE("Unit", '') <> 'do smaku'; + SELECT COUNT(*) INTO no_macros FROM diet."Recipes" WHERE "IsActive" AND ("Protein" IS NULL OR "Calories" IS NULL); + RAISE NOTICE 'Unlinked ingredient rows (grams, non-spice): %', unlinked; + RAISE NOTICE 'Recipes still missing macros: %', no_macros; +END $$; + +COMMIT; + +-- Reference mapping: database/scripts/005_ingredient_name_mapping.csv diff --git a/database/scripts/005_ingredient_name_mapping.csv b/database/scripts/005_ingredient_name_mapping.csv new file mode 100644 index 0000000..711e232 --- /dev/null +++ b/database/scripts/005_ingredient_name_mapping.csv @@ -0,0 +1,279 @@ +IngredientName,CatalogName,Status +Awokado,Avocado,mapped +Bakłażan,Eggplant,mapped +Banan,Banana,mapped +Bataty,Potato,mapped +Bazylia suszona,Basil dried,mapped +"Bazylia suszona, oregano, płatki chili, sół, pieprz",,skip_spice +Bazylia świeża,Basil fresh,mapped +"Bazylia świeża lub suszona, sól, pieprz",UNMAPPED,unmapped +"Bazylia świeża, pieprz",UNMAPPED,unmapped +"Bazylia świeża, pieprz czarny",UNMAPPED,unmapped +"Bazylia, oregano, pieprz",UNMAPPED,unmapped +"Bazylia, oregano, sól, pieprz",UNMAPPED,unmapped +"Bazylia, rozmaryn, sól, pieprz",UNMAPPED,unmapped +"Bazylia, sól, pieprz",UNMAPPED,unmapped +Borówki amerykańskie,Blueberries,mapped +Brokuł,Broccoli,mapped +Brokuły,Broccoli,mapped +Brzoskwinia,Peach nectarine,mapped +Brzoskwinia (świeża lub mrożona),Peach nectarine,mapped +Bułka tarta pełnoziarnista,Breadcrumbs whole grain,mapped +Cebula,Onion,mapped +Cebula czerwona,Onion,mapped +Chleb pita pełnoziarnisty,Whole wheat bread,mapped +Chleb żytni razowy,Rye bread whole grain,mapped +Ciecierzyca z puszki,Chickpeas (cooked),mapped +Ciecierzyca z puszki (odsączona),Chickpeas (cooked),mapped +Cukinia,Zucchini,mapped +Cynamon,Cinnamon ground,mapped +Cytryna,Lemon juice,mapped +Cytryna (sok),Lemon juice,mapped +Czosnek,Garlic,mapped +Czosnek granulowany,Garlic,mapped +"Czosnek, sól, pieprz",Garlic,mapped +"Czosnek, sół, pieprz",Garlic,mapped +"Czosnek, tymianek, sól, pieprz",Garlic,mapped +Dorsz świeży filety bez skóry,Cod,mapped +Dynia pestki łuskane,Pumpkin seeds,mapped +Dżem truskawkowy niskosłodzony,Strawberries,mapped +Erytrol/Erytrytol,,skip_spice +Fasola czerwona w zalewie (konserwowa),Kidney beans (cooked),mapped +Fasola szparagowa,Green beans,mapped +Fasolka szparagowa,Green beans,mapped +Fasolka szparagowa mrożona,Green beans,mapped +Filet z dorsza lub dorada,Cod,mapped +Filet z piersi indyka,Turkey breast,mapped +Filet z piersi indyka (rozbite plastry),Turkey breast,mapped +Filet z łososia,Salmon,mapped +"Gałka muszkatołowa, sól, pieprz",,skip_spice +"Gałka muszkatołowa, sół, pieprz",,skip_spice +Gruszka,Peach nectarine,mapped +Hummus,Hummus,mapped +Imbir mielony,Ginger ground,mapped +Jabłko,Apple,mapped +Jaja kurze całe,Chicken egg (whole),mapped +Jaja kurze całe (na twardo),Chicken egg (whole),mapped +Jajko,Chicken egg (whole),mapped +Jajko (do klopsików),Chicken egg (whole),mapped +Jajko (do panierki),Chicken egg (whole),mapped +Jajko ugotowane na twardo,Chicken egg (whole),mapped +Jogurt grecki 2%,Greek yogurt (plain),mapped +Jogurt naturalny,Greek yogurt (plain),mapped +Kabanosy z kurczaka,Chicken breast,mapped +Kakao 16% proszek,Cocoa powder 16% fat,mapped +Kalafior,Cauliflower,mapped +Kapary,Capers,mapped +Kasza bulgur,Bulgur dry,mapped +Kasza gryczana,Buckwheat groats dry,mapped +Kasza jaglana,Buckwheat groats dry,mapped +Keczup,Ketchup,mapped +Kiełbasa chorizo,Ham (cooked),mapped +Kiełki brokuła,Broccoli,mapped +Kiwi,Kiwi,mapped +"Kminek mielony, papryka słodka, sól, pieprz",,skip_spice +"Kminek mielony, płatki chili, sól, pieprz",,skip_spice +"Kminek, papryka słodka, chili, sól",,skip_spice +Kolendra suszone liście,Coriander dried,mapped +Koncentrat pomidorowy 30%,Tomato passata,mapped +Koper ogrodowy,Dill fresh,mapped +Ksylitol,,skip_spice +Kukurydza konserwowa,Sweet corn canned,mapped +"Kurkuma, kminek, bazylia, sól, pieprz",UNMAPPED,unmapped +"Kurkuma, kminek, cynamon, kolendra suszona, imbir mielony, sól, pieprz",UNMAPPED,unmapped +"Kurkuma, kminek, cynamon, kolendra suszona, sól, pieprz",UNMAPPED,unmapped +"Kurkuma, kminek, kolendra suszona, sól",UNMAPPED,unmapped +Kuskus pełnoziarnisty,Couscous dry,mapped +Len mielony,Flax seeds ground,mapped +Len nasiona,Flax seeds ground,mapped +"Len, nasiona",Flax seeds ground,mapped +Liść laurowy,,skip_spice +Majeranek,Marjoram dried,mapped +Makaron fusilli pełnoziarnisty,Pasta whole grain dry,mapped +Makaron penne pełnoziarnisty,Pasta whole grain dry,mapped +Makaron pełnoziarnisty,Pasta whole grain dry,mapped +Makaron pełnoziarnisty (fusilli),Pasta whole grain dry,mapped +Makaron pełnoziarnisty (penne lub spaghetti),Pasta whole grain dry,mapped +Makaron pełnoziarnisty (tagliatelle lub penne),Pasta whole grain dry,mapped +Makaron rigatoni pełnoziarnisty,Pasta whole grain dry,mapped +Makaron spaghetti pełnoziarnisty,Pasta whole grain dry,mapped +Makaron tagliatelle pełnoziarnisty,Pasta whole grain dry,mapped +Maliny,Raspberries,mapped +Maliny (świeże lub mrożone),Raspberries,mapped +Maliny świeże lub mrożone,Raspberries,mapped +Mango (świeże lub mrożone),Mango,mapped +Marchew,Carrot,mapped +Marchewka,Carrot,mapped +Masło,Butter,mapped +Masło ekstra,Butter,mapped +Masło orzechowe,Butter,mapped +Mielona gałka muszkatołowa,,skip_spice +Mielona papryka chili,,skip_spice +Mielona słodka papryka,,skip_spice +Mielony filet z indyka,,skip_spice +Mielony filet z piersi indyka (bez skóry),,skip_spice +Migdały,Almonds,mapped +Migdały w płatkach,Almonds,mapped +Mix sałat,Lettuce mix,mapped +Miód,Honey,mapped +Miód pszczeli,Honey,mapped +Mięso wołowe (świeżo mielone z rostbefu),Ground beef (15% fat),mapped +Mięso wołowe mielone,Ground beef (15% fat),mapped +Mięso z piersi kurczaka bez skóry,Chicken breast,mapped +Mięso z podudzia indyka bez skóry,Turkey breast,mapped +Mleko 2%,Milk 2%,mapped +Mleko spożywcze 2%,Milk 2%,mapped +Morszczuk świeży,Cod,mapped +Musztarda dijonska,Mustard dijon,mapped +Mąka pszenna (do obtoczenia),Wheat flour type 1850,mapped +Mąka pszenna typ 1850,Wheat flour type 1850,mapped +Nasiona chia,Chia seeds,mapped +Natka pietruszki,Parsley fresh,mapped +Nektarynka,Peach nectarine,mapped +Noga (udo) kurczaka,Chicken breast,mapped +Ocet jabłkowy z dojrzałych jabłek,Apple,mapped +"Ocet winny biały, sól, pieprz",UNMAPPED,unmapped +Olej lniany tłoczony na zimno,Flaxseed oil,mapped +Oliwa z oliwek,Olive oil,mapped +Oliwki czarne,Olives black,mapped +Oliwki czarne i zielone (mix),Olives black,mapped +Oliwki zielone,Olives green,mapped +Oliwki zielone marynowane,Olives green,mapped +Oregano suszone,Oregano dried,mapped +"Oregano, bazylia suszona, płatki chili, sól, pieprz",,skip_spice +"Oregano, bazylia, sól",UNMAPPED,unmapped +"Oregano, bazylia, sól, pieprz",UNMAPPED,unmapped +"Oregano, pieprz",UNMAPPED,unmapped +"Oregano, płatki chili, sól, pieprz",,skip_spice +"Oregano, sok cytryny, sól, pieprz",Lemon juice,mapped +"Oregano, sól, pieprz",UNMAPPED,unmapped +"Oregano, tymianek, czosnek granulowany, sól",Garlic,mapped +"Oregano, tymianek, sól, pieprz",UNMAPPED,unmapped +Orzechy nerkowca,Cashews,mapped +Orzechy nerkowca (bez soli),Cashews,mapped +Orzechy włoskie,Walnuts,mapped +Papryka czerwona,Bell pepper,mapped +"Papryka słodka, chili, oregano, sól, pieprz",,skip_spice +Parmezan tarty,Parmesan,mapped +Parmezan w płatkach,Parmesan,mapped +Passata pomidorowa,Tomato passata,mapped +Passata pomidorowa (przecier),Tomato passata,mapped +Passata pomidorowa przecier,Tomato passata,mapped +Pestki granatu,Pomegranate seeds,mapped +Pesto bazyliowe,Pesto basil,mapped +Pieczarka uprawna świeża,Mushrooms,mapped +Pieczarki świeże,Mushrooms,mapped +Pieprz czarny mielony,Black pepper ground,mapped +Pierś z indyka,Turkey breast,mapped +Pierś z kurczaka,Chicken breast,mapped +Pierś z kurczaka (rozbita cienko),Chicken breast,mapped +Pierś z kurczaka (ugotowana lub z poprzedniego dnia),Chicken breast,mapped +Pierś z kurczaka (z poprzedniego dnia lub świeża),Chicken breast,mapped +Pietruszka korzeń,Celery root,mapped +Pietruszka liście,Parsley fresh,mapped +"Pietruszka, liście",Parsley fresh,mapped +Piniole,Pine nuts,mapped +Pistacje (łuskane),Pistachios,mapped +Pistacje łuskane,Pistachios,mapped +Polędwica sopocka,Ham (cooked),mapped +Polędwica wieprzowa,Pork tenderloin,mapped +Polędwica wieprzowa surowa,Pork tenderloin,mapped +Pomarańcza,Orange,mapped +Pomidor,Tomato,mapped +Pomidory cherry,Tomato,mapped +Pomidory koktajlowe,Tomato,mapped +Pomidory z puszki,Tomato passata,mapped +Pomidory z puszki (krojone),Tomato passata,mapped +Por,Leek,mapped +Proszek do pieczenia,,skip_spice +Przyprawa do drobiu,,skip_spice +"Płatki chili, sól",,skip_spice +"Płatki chili, sól, pieprz",,skip_spice +Płatki owsiane,Oats (dry),mapped +Ricotta,Ricotta,mapped +Rozmaryn,Rosemary dried,mapped +"Rozmaryn, tymianek, bazylia, sól, pieprz",UNMAPPED,unmapped +"Rozmaryn, tymianek, oregano, sól, pieprz",UNMAPPED,unmapped +"Rozmaryn, tymianek, sól, pieprz",UNMAPPED,unmapped +Rukola,Lettuce mix,mapped +Ryż basmati,White rice dry,mapped +Ryż brązowy,Brown rice dry,mapped +Ryż jaśminowy,White rice dry,mapped +Rzodkiewka,Radish,mapped +Salami,Ham (cooked),mapped +Salami (cienkie plastry),Ham (cooked),mapped +Sałata,Lettuce mix,mapped +Sałata lub mix sałat,Lettuce mix,mapped +Seler korzeniowy,Celery root,mapped +Seler naciowy,Celery stalk,mapped +Ser feta,Feta cheese,mapped +Ser gouda tłusty,Parmesan,mapped +Ser mozzarella,Mozzarella,mapped +Ser parmezan (tarty),Parmesan,mapped +Ser parmezan tarty,Parmesan,mapped +Ser twarogowy półtłusty,Cottage cheese,mapped +Ser typu feta,Feta cheese,mapped +Serek wiejski naturalny,Greek yogurt (plain),mapped +Siemię lniane mielone,Flax seeds ground,mapped +Siemię lniane świeżo mielone,Flax seeds ground,mapped +Soczewica zielona,Lentils (cooked),mapped +Soczewica zielona (ugotowana),Lentils (cooked),mapped +Sok cytrynowy,Lemon juice,mapped +Sok marchwiowy,Carrot juice,mapped +Sos sojowy,Soy sauce,mapped +Sos sojowy ciemny,Soy sauce,mapped +Suszone figi,Dried figs,mapped +Suszone morele,Dried apricots,mapped +Suszone pomidory (odsączone),Sun-dried tomatoes in oil,mapped +"Suszone pomidory (w oleju z ziołami, odsączone)",Sun-dried tomatoes in oil,mapped +Suszone pomidory w oliwie (odsączone),Sun-dried tomatoes in oil,mapped +Szczypiorek,Chives,mapped +Szpinak,Spinach,mapped +Szpinak świeży,Spinach,mapped +Szynka parmeńska,Ham (cooked),mapped +Szynka parmeńska (Prosciutto),Ham (cooked),mapped +Szynka parmeńska (prosciutto),Ham (cooked),mapped +Szynka z indyka,Turkey breast,mapped +Szynka z indyka (bez konserwantów),Turkey breast,mapped +Sól himalajska,,skip_spice +"Sól, pieprz",UNMAPPED,unmapped +"Sól, pieprz, oregano",UNMAPPED,unmapped +"Sól, pieprz, płatki chili",,skip_spice +"Sól, pieprz, zioła prowansalskie",,skip_spice +Sół himalajska,,skip_spice +"Sół, pieprz, płatki chili",,skip_spice +Słonecznik nasiona łuskane,Pumpkin seeds,mapped +Tahini,Tahini,mapped +Tortilla pełnoziarnista,Whole wheat tortilla,mapped +Truskawki,Strawberries,mapped +Truskawki (świeże lub mrożone),Strawberries,mapped +Tuńczyk w wodzie (odsączony),Tuna (canned in water),mapped +Twaróg półtłusty,Cottage cheese,mapped +Tymianek,Thyme dried,mapped +"Tymianek, czosnek granulowany, sól",Garlic,mapped +"Tymianek, czosnek granulowany, sól, pieprz",Garlic,mapped +"Tymianek, oregano, sól, pieprz",UNMAPPED,unmapped +"Tymianek, rozmaryn, papryka słodka, sół, pieprz",,skip_spice +"Tymianek, rozmaryn, sól, pieprz",UNMAPPED,unmapped +"Tymianek, rozmaryn, zioła prowansalskie, sól, pieprz",,skip_spice +"Tymianek, sól, pieprz",UNMAPPED,unmapped +"Tymianek, sół, pieprz",UNMAPPED,unmapped +Udka kurczaka (bez skóry),Chicken breast,mapped +Udka kurczaka bez skóry,Chicken breast,mapped +Wiórki kokosowe,Coconut flakes,mapped +Woda,Water,mapped +"Wołowina (rostbef lub ligawa, cienkie paski)",Beef stew meat,mapped +Wołowina gulaszowa,Beef stew meat,mapped +Wołowina gulaszowa (karkówka/łopatka),Beef stew meat,mapped +Wołowina mielona,Ground beef (15% fat),mapped +Wątróbka kurczaka,Chicken breast,mapped +Ziele angielskie,,skip_spice +Ziemniaki,Potato,mapped +Zioła prowansalskie,,skip_spice +"Zioła prowansalskie, rozmaryn, tymianek, sól, pieprz",,skip_spice +"Zioła prowansalskie, sól, pieprz",,skip_spice +"Zioła prowansalskie, tymianek, liść laurowy, sól, pieprz",,skip_spice +"Zioła prowansalskie, tymianek, sól, pieprz",,skip_spice +Łosoś wędzony,Smoked salmon,mapped +Śmietana 12%,Sour cream 12%,mapped +Żurawina suszona,Dried cranberries,mapped diff --git a/database/scripts/006_diet_and_recipe_generators.sql b/database/scripts/006_diet_and_recipe_generators.sql new file mode 100644 index 0000000..998d77a --- /dev/null +++ b/database/scripts/006_diet_and_recipe_generators.sql @@ -0,0 +1,110 @@ +-- 006_diet_and_recipe_generators.sql +-- AI-assisted diet plan and recipe generation modules. +-- Apply after scripts 001–005. + +BEGIN; + +-- --------------------------------------------------------------------------- +-- User profile for TDEE / goal-based planning +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DietUserProfiles" ( + "UserId" uuid PRIMARY KEY + REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "HeightCm" numeric(5,2) NOT NULL CHECK ("HeightCm" > 0), + "WeightKg" numeric(5,2) NOT NULL CHECK ("WeightKg" > 0), + "Age" int NOT NULL CHECK ("Age" BETWEEN 10 AND 120), + "Sex" varchar(16) NOT NULL CHECK ("Sex" IN ('male', 'female')), + "ActivityLevel" varchar(32) NOT NULL + CHECK ("ActivityLevel" IN ('sedentary', 'light', 'moderate', 'active', 'very_active')), + "Goal" varchar(32) NOT NULL + CHECK ("Goal" IN ('lose_weight', 'maintain', 'gain_muscle', 'recomp')), + "AllergiesNotes" text NULL, + "DislikedFoods" text NULL, + "MealsPerDayMask" int NOT NULL DEFAULT 15 CHECK ("MealsPerDayMask" BETWEEN 1 AND 31), + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL +); + +COMMENT ON COLUMN diet."DietUserProfiles"."MealsPerDayMask" IS 'Bitmask: bit0=Breakfast, bit1=SecondBreakfast, bit2=Lunch, bit3=Dinner, bit4=Snack'; + +-- --------------------------------------------------------------------------- +-- Diet generator wizard sessions +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DietGeneratorSessions" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "Status" varchar(32) NOT NULL DEFAULT 'macros_pending', + "ProposedCalories" int NULL CHECK ("ProposedCalories" IS NULL OR "ProposedCalories" > 0), + "ProposedProteinG" numeric(6,2) NULL, + "ProposedFatG" numeric(6,2) NULL, + "ProposedCarbsG" numeric(6,2) NULL, + "MacroRationale" text NULL, + "MacrosAcceptedAt" timestamptz NULL, + "PlanStartDate" date NULL, + "PlanDayCount" int NOT NULL DEFAULT 7 CHECK ("PlanDayCount" BETWEEN 1 AND 28), + "CalorieToleranceKcal" int NOT NULL DEFAULT 10 CHECK ("CalorieToleranceKcal" >= 0), + "PreferencesJson" jsonb NULL, + "CommittedAt" timestamptz NULL, + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL +); + +CREATE INDEX IF NOT EXISTS "IX_DietGeneratorSessions_UserId_Status" + ON diet."DietGeneratorSessions" ("UserId", "Status"); + +-- --------------------------------------------------------------------------- +-- Draft meals (review before commit to MealPlanEntries) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."DietGeneratorDraftMeals" ( + "Id" serial PRIMARY KEY, + "SessionId" int NOT NULL REFERENCES diet."DietGeneratorSessions"("Id") ON DELETE CASCADE, + "PlanDate" date NOT NULL, + "MealCategory" int NOT NULL CHECK ("MealCategory" BETWEEN 0 AND 4), + "RecipeId" int NOT NULL REFERENCES diet."Recipes"("Id") ON DELETE CASCADE, + "Portions" numeric(4,2) NOT NULL DEFAULT 1 CHECK ("Portions" > 0), + "Calories" int NULL, + "ProteinG" numeric(6,2) NULL, + "FatG" numeric(6,2) NULL, + "CarbsG" numeric(6,2) NULL, + "IsLocked" boolean NOT NULL DEFAULT false, + "GenerationVersion" int NOT NULL DEFAULT 1, + UNIQUE ("SessionId", "PlanDate", "MealCategory") +); + +CREATE INDEX IF NOT EXISTS "IX_DietGeneratorDraftMeals_SessionId" + ON diet."DietGeneratorDraftMeals" ("SessionId"); + +-- --------------------------------------------------------------------------- +-- Recipe generator sessions (draft JSON until commit) +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS diet."RecipeGeneratorSessions" ( + "Id" serial PRIMARY KEY, + "UserId" uuid NOT NULL REFERENCES diet."AspNetUsers"("Id") ON DELETE CASCADE, + "Status" varchar(32) NOT NULL DEFAULT 'draft', + "ConstraintsJson" jsonb NOT NULL DEFAULT '{}', + "DraftJson" jsonb NULL, + "SavedRecipeId" int NULL REFERENCES diet."Recipes"("Id") ON DELETE SET NULL, + "GenerationVersion" int NOT NULL DEFAULT 1, + "CreatedAt" timestamptz NOT NULL DEFAULT now(), + "UpdatedAt" timestamptz NULL +); + +CREATE INDEX IF NOT EXISTS "IX_RecipeGeneratorSessions_UserId" + ON diet."RecipeGeneratorSessions" ("UserId"); + +-- --------------------------------------------------------------------------- +-- Recipe provenance (AI vs manual) +-- --------------------------------------------------------------------------- +ALTER TABLE diet."Recipes" + ADD COLUMN IF NOT EXISTS "Source" varchar(32) NOT NULL DEFAULT 'manual'; + +ALTER TABLE diet."Recipes" + ADD COLUMN IF NOT EXISTS "CreatedByUserId" uuid NULL + REFERENCES diet."AspNetUsers"("Id") ON DELETE SET NULL; + +ALTER TABLE diet."Recipes" + ADD COLUMN IF NOT EXISTS "IsDraft" boolean NOT NULL DEFAULT false; + +COMMENT ON COLUMN diet."Recipes"."Source" IS 'manual | excel | ai_generated'; + +COMMIT; diff --git a/database/scripts/007_remove_duplicate_recipes_by_name.sql b/database/scripts/007_remove_duplicate_recipes_by_name.sql new file mode 100644 index 0000000..0a8ed2b --- /dev/null +++ b/database/scripts/007_remove_duplicate_recipes_by_name.sql @@ -0,0 +1,135 @@ +-- 007_remove_duplicate_recipes_by_name.sql +-- Removes duplicate recipes that share the same "Name" (trimmed, exact match). +-- Keeps the row with the lowest "Id" for each name. +-- +-- Analysis of Recipes-2.csv export (196 rows): +-- 168 unique names, 28 duplicate groups, 28 rows to delete (Ids 113–140). +-- Those duplicate Ids mirror Ids 29–56 one-to-one. +-- +-- Run inside a transaction. Review the preview queries before COMMIT. + +BEGIN; + +-- --------------------------------------------------------------------------- +-- 1) Preview duplicates +-- --------------------------------------------------------------------------- +-- SELECT +-- r."Name", +-- array_agg(r."Id" ORDER BY r."Id") AS ids, +-- min(r."Id") AS keep_id, +-- array_agg(r."Id" ORDER BY r."Id") FILTER (WHERE r."Id" <> min(r."Id")) AS remove_ids +-- FROM diet."Recipes" r +-- GROUP BY r."Name" +-- HAVING count(*) > 1 +-- ORDER BY min(r."Id"); + +-- --------------------------------------------------------------------------- +-- 2) Build duplicate map: remove_id -> keep_id +-- --------------------------------------------------------------------------- +CREATE TEMP TABLE recipe_dedup_map ON COMMIT DROP AS +SELECT + r."Id" AS remove_id, + d.keep_id +FROM diet."Recipes" r +INNER JOIN ( + SELECT btrim("Name") AS name_key, MIN("Id") AS keep_id + FROM diet."Recipes" + GROUP BY btrim("Name") + HAVING COUNT(*) > 1 +) d ON btrim(r."Name") = d.name_key +WHERE r."Id" <> d.keep_id; + +-- Optional: restrict to known duplicate ids from export +-- DELETE FROM recipe_dedup_map WHERE remove_id NOT BETWEEN 113 AND 140; + +DO $$ +DECLARE + dup_count int; +BEGIN + SELECT count(*) INTO dup_count FROM recipe_dedup_map; + RAISE NOTICE 'Duplicate recipe rows to remove: %', dup_count; +END $$; + +-- --------------------------------------------------------------------------- +-- 3) Re-point foreign keys to the canonical (lowest-id) recipe +-- --------------------------------------------------------------------------- + +-- Meal planner / diet generator drafts +UPDATE diet."MealPlanEntries" e +SET "RecipeId" = m.keep_id, + "UpdatedAt" = now() +FROM recipe_dedup_map m +WHERE e."RecipeId" = m.remove_id; + +UPDATE diet."DietGeneratorDraftMeals" d +SET "RecipeId" = m.keep_id +FROM recipe_dedup_map m +WHERE d."RecipeId" = m.remove_id; + +-- AI recipe generator saved draft pointer +UPDATE diet."RecipeGeneratorSessions" s +SET "SavedRecipeId" = m.keep_id, + "UpdatedAt" = now() +FROM recipe_dedup_map m +WHERE s."SavedRecipeId" = m.remove_id; + +-- Diet tracking (nullable FK) +UPDATE diet."MealConsumptions" c +SET "RecipeId" = m.keep_id +FROM recipe_dedup_map m +WHERE c."RecipeId" = m.remove_id; + +-- Favorites: drop rows that would collide after merge, then re-point the rest +DELETE FROM diet."UserFavoriteRecipes" f +USING recipe_dedup_map m +WHERE f."RecipeId" = m.remove_id + AND EXISTS ( + SELECT 1 + FROM diet."UserFavoriteRecipes" f2 + WHERE f2."UserId" = f."UserId" + AND f2."RecipeId" = m.keep_id + ); + +UPDATE diet."UserFavoriteRecipes" f +SET "RecipeId" = m.keep_id +FROM recipe_dedup_map m +WHERE f."RecipeId" = m.remove_id; + +-- --------------------------------------------------------------------------- +-- 4) Delete duplicate recipes (Ingredients + RecipeSteps cascade) +-- --------------------------------------------------------------------------- +DELETE FROM diet."Recipes" r +USING recipe_dedup_map m +WHERE r."Id" = m.remove_id; + +-- --------------------------------------------------------------------------- +-- 5) Verify +-- --------------------------------------------------------------------------- +DO $$ +DECLARE + remaining_dup_names int; + recipe_count int; +BEGIN + SELECT count(*) INTO remaining_dup_names + FROM ( + SELECT btrim("Name") + FROM diet."Recipes" + GROUP BY btrim("Name") + HAVING count(*) > 1 + ) d; + + SELECT count(*) INTO recipe_count FROM diet."Recipes"; + + IF remaining_dup_names > 0 THEN + RAISE EXCEPTION 'Duplicate recipe names still present: % groups', remaining_dup_names; + END IF; + + RAISE NOTICE 'Done. Active recipe count: %', recipe_count; +END $$; + +COMMIT; + +-- Optional hardening (uncomment if every recipe name must be unique going forward): +-- CREATE UNIQUE INDEX IF NOT EXISTS "UX_Recipes_Name" +-- ON diet."Recipes" (btrim("Name")) +-- WHERE "IsActive" AND NOT "IsDraft"; diff --git a/database/scripts/008_diet_draft_meal_ingredients.sql b/database/scripts/008_diet_draft_meal_ingredients.sql new file mode 100644 index 0000000..5d758d7 --- /dev/null +++ b/database/scripts/008_diet_draft_meal_ingredients.sql @@ -0,0 +1,25 @@ +-- 008_diet_draft_meal_ingredients.sql +-- Scaled ingredient amounts per diet generator draft meal (portions applied at plan time). +-- Apply after script 006. + +BEGIN; + +CREATE TABLE IF NOT EXISTS diet."DietGeneratorDraftMealIngredients" ( + "Id" serial PRIMARY KEY, + "DraftMealId" int NOT NULL + REFERENCES diet."DietGeneratorDraftMeals"("Id") ON DELETE CASCADE, + "SourceIngredientId" int NULL + REFERENCES diet."Ingredients"("Id") ON DELETE SET NULL, + "Name" varchar(255) NOT NULL, + "AmountGrams" numeric(8,2) NULL, + "Unit" varchar(50) NULL, + "SortOrder" int NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS "IX_DietGeneratorDraftMealIngredients_DraftMealId" + ON diet."DietGeneratorDraftMealIngredients" ("DraftMealId"); + +COMMENT ON TABLE diet."DietGeneratorDraftMealIngredients" IS + 'Ingredient weights recalculated for the planned portion size of each draft meal.'; + +COMMIT; diff --git a/database/scripts/009_fix_identity_sequences.sql b/database/scripts/009_fix_identity_sequences.sql new file mode 100644 index 0000000..a092352 --- /dev/null +++ b/database/scripts/009_fix_identity_sequences.sql @@ -0,0 +1,27 @@ +-- Resync PostgreSQL identity sequences after bulk imports or manual ID inserts. +-- Safe to run multiple times. Run against the modwad database when new rows fail with: +-- duplicate key value violates unique constraint "Recipes_pkey" + +SELECT setval( + pg_get_serial_sequence('diet."Recipes"', 'Id'), + COALESCE((SELECT MAX("Id") FROM diet."Recipes"), 1), + (SELECT MAX("Id") IS NOT NULL FROM diet."Recipes") +); + +SELECT setval( + pg_get_serial_sequence('diet."Ingredients"', 'Id'), + COALESCE((SELECT MAX("Id") FROM diet."Ingredients"), 1), + (SELECT MAX("Id") IS NOT NULL FROM diet."Ingredients") +); + +SELECT setval( + pg_get_serial_sequence('diet."RecipeSteps"', 'Id'), + COALESCE((SELECT MAX("Id") FROM diet."RecipeSteps"), 1), + (SELECT MAX("Id") IS NOT NULL FROM diet."RecipeSteps") +); + +SELECT setval( + pg_get_serial_sequence('diet."RecipeGeneratorSessions"', 'Id'), + COALESCE((SELECT MAX("Id") FROM diet."RecipeGeneratorSessions"), 1), + (SELECT MAX("Id") IS NOT NULL FROM diet."RecipeGeneratorSessions") +); diff --git a/database/scripts/README.md b/database/scripts/README.md new file mode 100644 index 0000000..e85474c --- /dev/null +++ b/database/scripts/README.md @@ -0,0 +1,173 @@ +# Database scripts + +SQL in this folder is applied **manually** on PostgreSQL. The API is database-first and does not run migrations. + +## Diet tracking module + +Run on your DailyMeals database: + +```bash +psql "$DATABASE_URL" -f database/scripts/001_diet_tracking.sql +``` + +### Tables created + +| Table | Purpose | +|-------|---------| +| `UserDailyGoals` | Daily calorie, macro, and water targets (per user) | +| `DrinkCatalog` | Preset drinks (water, tea, coffee, …) with default volumes | +| `MealConsumptions` | Meals the user ate (optional link to `Recipes`, macros snapshotted) | +| `DrinkConsumptions` | Hydration / beverage log | +| `DietReminders` | Reminder schedules (water, meal, custom) | + +### API (after script + API deploy) + +| Method | Endpoint | +|--------|----------| +| GET | `/api/diet/summary?date=YYYY-MM-DD` | +| GET/PUT | `/api/diet/goals` | +| GET/POST/DELETE | `/api/diet/meals` | +| GET | `/api/diet/drinks/catalog` | +| GET/POST/DELETE | `/api/diet/drinks` | +| GET/POST/PUT/DELETE | `/api/diet/reminders` | +| GET | `/api/diet/reminders/due?date=&time=` | + +Web UI: **My Diet** at `/diet` in the React frontend. + +## Ingredient nutrition catalog + +```bash +psql "$DATABASE_URL" -f database/scripts/002_ingredient_nutrition_catalog.sql +``` + +### Tables created + +| Table | Purpose | +|-------|---------| +| `IngredientCategories` | Food groups (meat, vegetables, dairy, …) | +| `IngredientNutritionCatalog` | Per-100 g calories, protein, fat, carbs, fiber | + +### API (after script + API deploy) + +| Method | Endpoint | +|--------|----------| +| GET | `/api/ingredient-catalog/categories` | +| GET | `/api/ingredient-catalog?category=&search=` | +| GET | `/api/ingredient-catalog/{id}` | +| POST | `/api/ingredient-catalog` | +| PUT | `/api/ingredient-catalog/{id}` | +| DELETE | `/api/ingredient-catalog/{id}` | + +Web UI: **Ingredient catalog** at `/ingredients` in all frontends. + +### Polish ingredient names + +```bash +psql "$DATABASE_URL" -f database/scripts/003_ingredient_catalog_polish_names.sql +``` + +Adds `NamePl` column and Polish display names for all seeded catalog items. Search matches both English and Polish names. + +## Extended features (meal planner, auth persistence, catalog linking) + +```bash +psql "$DATABASE_URL" -f database/scripts/004_extended_features.sql +``` + +### Changes + +| Object | Purpose | +|--------|---------| +| `RefreshTokens` | Persist web login sessions across API restarts | +| `MealPlanEntries` | Weekly meal planner (one recipe per meal slot per day) | +| `UserFavoriteRecipes` | Quick-log favorites | +| `Ingredients.CatalogItemId` | FK to nutrition catalog for auto macro calculation | + +Run **after** scripts 001–003. + +## Catalog expansion + bulk ingredient linking + +One-time migration for existing recipe data (based on your `Ingredients.csv` export): + +```bash +psql "$DATABASE_URL" -f database/scripts/005_catalog_expansion_and_ingredient_linking.sql +``` + +### What it does + +1. **Adds ~70 catalog items** missing from the seed (Polish staples: rye bread, dry pasta/rice, feta, tahini, passata, herbs/spices, …). +2. **Links `Ingredients.CatalogItemId`** via a name→catalog mapping (~218 ingredient name variants). +3. **Recalculates `Recipes` macros** as `SUM(catalog × AmountGrams / 100)` for all recipes with linked ingredients. + +Reference mapping: `database/scripts/005_ingredient_name_mapping.csv` (ingredient name → catalog name → status). + +After the script, check PostgreSQL notices for unlinked rows and recipes still missing macros. Spice rows (`do smaku`, `szczypta`) and rows without grams are intentionally skipped. + +### API alternative (after linking) + +If ingredients already have `CatalogItemId` set, you can recalculate without re-running SQL: + +| Method | Endpoint | +|--------|----------| +| POST | `/api/recipes/recalculate-macros` | + +Returns `{ recipesProcessed, recipesUpdated, unlinkedIngredientRows }`. Requires auth. Overwrites recipe macros when at least one linked ingredient has grams. + +## AI generators (diet plan + recipes) + +```bash +psql "$DATABASE_URL" -f database/scripts/006_diet_and_recipe_generators.sql +``` + +Configure OpenAI in `MealPlan.Api/appsettings.Local.json`: + +```json +"OpenAI": { + "ApiKey": "sk-...", + "Model": "gpt-4.1-mini", + "MacrosModel": "gpt-4.1-mini", + "PlanModel": "gpt-4.1-mini", + "RecipeModel": "gpt-4.1", + "MaxTokens": 4096 +} +``` + +| Setting | Used for | +|---------|----------| +| `MacrosModel` | Diet generator — refine daily kcal/macros | +| `PlanModel` | Diet generator — pick recipes for the week | +| `RecipeModel` | Recipe generator — ingredients + steps | +| `Model` | Fallback when a task model is omitted | + +Without an API key, macro targets still use TDEE math and meal plans use a local greedy picker; recipe generation uses a simple fallback template. + +### Diet generator API + +| Method | Endpoint | +|--------|----------| +| GET/PUT | `/api/diet-generator/profile` | +| POST | `/api/diet-generator/sessions` | +| GET | `/api/diet-generator/sessions/{id}` | +| POST | `/api/diet-generator/sessions/{id}/propose-macros` | +| POST | `/api/diet-generator/sessions/{id}/accept-macros` | +| POST | `/api/diet-generator/sessions/{id}/generate-plan` | +| POST | `/api/diet-generator/sessions/{id}/regenerate-meal` | +| PATCH | `/api/diet-generator/sessions/{id}/meals/{mealId}` | +| POST | `/api/diet-generator/sessions/{id}/commit` | + +Commit writes `UserDailyGoals` + `MealPlanEntries`. Daily calorie tolerance defaults to **10 kcal**. + +### Recipe generator API + +| Method | Endpoint | +|--------|----------| +| POST | `/api/recipe-generator/sessions` | +| GET | `/api/recipe-generator/sessions/{id}` | +| POST | `/api/recipe-generator/sessions/{id}/generate` | +| POST | `/api/recipe-generator/sessions/{id}/regenerate` | +| PUT | `/api/recipe-generator/sessions/{id}/draft` | +| POST | `/api/recipe-generator/sessions/{id}/commit` | + +Saved recipes get `Source = ai_generated` and macros from the nutrition catalog. + +Web UI: `/diet-generator` and `/recipe-generator` in the React frontend. diff --git a/meal-plan-frontend-angular/.editorconfig b/meal-plan-frontend-angular/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/meal-plan-frontend-angular/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/meal-plan-frontend-angular/.gitignore b/meal-plan-frontend-angular/.gitignore new file mode 100644 index 0000000..cc7b141 --- /dev/null +++ b/meal-plan-frontend-angular/.gitignore @@ -0,0 +1,42 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/meal-plan-frontend-angular/.vscode/extensions.json b/meal-plan-frontend-angular/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/meal-plan-frontend-angular/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/meal-plan-frontend-angular/.vscode/launch.json b/meal-plan-frontend-angular/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/meal-plan-frontend-angular/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/meal-plan-frontend-angular/.vscode/tasks.json b/meal-plan-frontend-angular/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/meal-plan-frontend-angular/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/meal-plan-frontend-angular/Dockerfile b/meal-plan-frontend-angular/Dockerfile new file mode 100644 index 0000000..10934ce --- /dev/null +++ b/meal-plan-frontend-angular/Dockerfile @@ -0,0 +1,30 @@ +# ---------- Build stage ---------- +FROM node:20-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +COPY . . +RUN npm run build + +# ---------- Serve stage ---------- +FROM nginx:alpine AS final + +RUN apk add --no-cache openssl && \ + mkdir -p /etc/nginx/certs && \ + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout /etc/nginx/certs/server.key \ + -out /etc/nginx/certs/server.crt \ + -subj "/CN=dailymeals.local" && \ + chmod 600 /etc/nginx/certs/server.key + +COPY nginx.conf /etc/nginx/nginx.conf +COPY --from=build /app/dist/meal-plan-frontend-angular/browser /usr/share/nginx/html + +EXPOSE 80 443 + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget --no-verbose --no-check-certificate --tries=1 --spider https://localhost/ || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/meal-plan-frontend-angular/README.md b/meal-plan-frontend-angular/README.md new file mode 100644 index 0000000..366a47e --- /dev/null +++ b/meal-plan-frontend-angular/README.md @@ -0,0 +1,56 @@ +# DailyMeals — Angular frontend + +Angular 19 rewrite of the React `meal-plan-frontend` app. + +## Stack + +- Angular 19 (standalone components) +- Tailwind CSS 3 +- `@ngx-translate` (EN / PL) +- Reactive forms +- JWT auth with silent refresh (`AuthInterceptor`) + +## Development + +```bash +cd meal-plan-frontend-angular +npm install +npm start +``` + +App runs at **http://localhost:4200** with API proxied to `http://localhost:5000` (`proxy.conf.json`). + +Ensure the ASP.NET API is running locally before signing in. + +## Production build + +```bash +npm run build +``` + +Output: `dist/meal-plan-frontend-angular/browser/` + +## Docker + +```bash +docker build -t dailymeals-angular . +``` + +Uses nginx with HTTPS and `/api` reverse proxy (same pattern as the React frontend). + +## Routes + +| Path | Page | +|------|------| +| `/login`, `/register` | Auth (public) | +| `/` | Dashboard | +| `/breakfast` … `/dinner` | Category lists | +| `/all-meals` | Search, select, PDF export | +| `/manage-meals` | Manual add + Excel import | +| `/recipes/:id` | Recipe detail | + +## i18n + +Translation files: `public/i18n/en.json`, `public/i18n/pl.json` + +Language preference stored in `localStorage` key `dailymeals.lang`. diff --git a/meal-plan-frontend-angular/angular.json b/meal-plan-frontend-angular/angular.json new file mode 100644 index 0000000..4cc0e25 --- /dev/null +++ b/meal-plan-frontend-angular/angular.json @@ -0,0 +1,127 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "meal-plan-frontend-angular": { + "projectType": "application", + "schematics": { + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:component": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/meal-plan-frontend-angular", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.5MB", + "maximumError": "2MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "proxy.conf.json" + }, + "configurations": { + "production": { + "buildTarget": "meal-plan-frontend-angular:build:production" + }, + "development": { + "buildTarget": "meal-plan-frontend-angular:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": false + } +} diff --git a/meal-plan-frontend-angular/nginx.conf b/meal-plan-frontend-angular/nginx.conf new file mode 100644 index 0000000..5adc5f6 --- /dev/null +++ b/meal-plan-frontend-angular/nginx.conf @@ -0,0 +1,84 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + server_tokens off; + + gzip on; + gzip_vary on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_proxied any; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + image/svg+xml + font/woff2; + + upstream api_upstream { + server api:5000; + } + + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl; + http2 on; + server_name _; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + root /usr/share/nginx/html; + index index.html; + + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'" always; + + location /api/ { + proxy_pass http://api_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + location /i18n/ { + expires 1h; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/meal-plan-frontend-angular/package-lock.json b/meal-plan-frontend-angular/package-lock.json new file mode 100644 index 0000000..5648cda --- /dev/null +++ b/meal-plan-frontend-angular/package-lock.json @@ -0,0 +1,16757 @@ +{ + "name": "meal-plan-frontend-angular", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meal-plan-frontend-angular", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "@ngx-translate/core": "^18.0.0", + "@ngx-translate/http-loader": "^18.0.0", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.27", + "@angular/cli": "^19.2.27", + "@angular/compiler-cli": "^19.2.0", + "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.5.0", + "jasmine-core": "~5.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.14", + "typescript": "~5.7.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1902.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.27.tgz", + "integrity": "sha512-I2YW4Zn1818toEGKPPcbv8qqglegNnWT4A4GM28LPRg4rOYwPkZzy3PAB7EIJEwZIDQgz2I+Oy0pc56BWcFVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.27.tgz", + "integrity": "sha512-63dLzZeRAZiDOWegZHDG1L3Su4m7tfh2n4uV4fXhJYQeyblAQIrmz/dsyjn2cwR021neDbteLCn7MeysKj47/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.27", + "@angular-devkit/build-webpack": "0.1902.27", + "@angular-devkit/core": "19.2.27", + "@angular/build": "19.2.27", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.2.27", + "@vitejs/plugin-basic-ssl": "1.2.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.28.0", + "fast-glob": "3.3.3", + "http-proxy-middleware": "3.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.2", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.4", + "piscina": "4.8.0", + "postcss": "8.5.12", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.85.0", + "sass-loader": "16.0.5", + "semver": "7.7.1", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.39.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.105.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.28.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.27", + "@web/test-runner": "^0.20.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1902.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.27.tgz", + "integrity": "sha512-O3v9/x2+yJrXJIBG0x7o/EMSjRjJqWcJb8u5lHFKut9qCpQJ6oDeoxqGM5SWXIk4RGr3qbAsOragkR2Lsq0cRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.27", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.27.tgz", + "integrity": "sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.27.tgz", + "integrity": "sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/build": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.27.tgz", + "integrity": "sha512-kMcuTUxDcnTa+JF9SaxdY+b6pkGjtefxGp/VYblSbvlzQ1rVX4wH4V9LHHyXsTOxRRNGqRJ01g0Enhq1iA19mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1902.27", + "@babel/core": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.6", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.3.2", + "browserslist": "^4.23.0", + "esbuild": "0.28.0", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.4", + "piscina": "4.8.0", + "rollup": "4.59.0", + "sass": "1.85.0", + "semver": "7.7.1", + "source-map-support": "0.5.21", + "vite": "6.4.2", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.2.6" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0 || ^19.2.0-next.0", + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "@angular/localize": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", + "@angular/ssr": "^19.2.27", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^19.0.0 || ^19.2.0-next.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "typescript": ">=5.5 <5.9" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/@angular/cli": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.27.tgz", + "integrity": "sha512-npqpoV7Y49ggwUysXbRUDDmFRmwQdZ92nljtxX1yjakmwR93WdEq7NwqHEGODOAXo5NtPoEeBNc3InPtrLmh5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1902.27", + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "@inquirer/prompts": "7.3.2", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.2.27", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.2", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.10", + "semver": "7.7.1", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.25.tgz", + "integrity": "sha512-WteFLyfDPvilzpSGHk64bqowa4NnHdbjNl1xXeGr038fgTjp0l0NdiUPqeDtYT4pf5lWl6477pPoQN7o//NyyQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.2.25", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.25.tgz", + "integrity": "sha512-GGEeTTd/DV71E07K0u5753bxXozc6Z7iWaCZJDW7xiA7hdHCwrYBMz1PVUJAtXcxf4wTdiXQR48kw8/YHYnDAw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.25.tgz", + "integrity": "sha512-IBBKRz6ua+y6bdcaUIpOPq7G/S4biJfcT1TCMD7wi+48wiKKSXj4J/8BN5fSjt+UozlHwya3B8z0XqnwO2ywZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.26.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.2.25", + "typescript": ">=5.5 <5.9" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.25.tgz", + "integrity": "sha512-j7/irbbdO4rmZfRXS+sphCerzgcRlpGFwxHc/76Ual+ckwJG0MPsNFkllz2SIEZzE1EZkmIunGrHbjeRBJjyvg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.25.tgz", + "integrity": "sha512-lbuhZiuNKy8vcIHFSP7sOW5kfHSVDiVsXh2/+rWjqGklscfUG5cTK9F9d+k60CH24OhK0NgRKoCHiAK5zPQi2g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.25", + "@angular/core": "19.2.25", + "@angular/platform-browser": "19.2.25", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.25.tgz", + "integrity": "sha512-UF7cyBnMF3puA/cGy3MXbiPB2Rlw0Umf78XH664Iwqsb37A+TxqzOPaByXsNTIbpV6/h28yO3dMvDVFT3aOvOA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.2.25", + "@angular/common": "19.2.25", + "@angular/core": "19.2.25" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.2.25.tgz", + "integrity": "sha512-T6LK+NH6VR0bNKepN6urSHPE86OxbIMvgm855W06YjXvK7cMotPCC+N10zv7M8zQRZtQ7WBkY0EGpC8qLrao0g==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.25", + "@angular/compiler": "19.2.25", + "@angular/core": "19.2.25", + "@angular/platform-browser": "19.2.25" + } + }, + "node_modules/@angular/router": { + "version": "19.2.25", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.25.tgz", + "integrity": "sha512-IddC2vPiagB2jAsUrNuAr74p+OirWaroouvPkUjzPmyQ+12/0EsygHamsgUbsCVLXJKh72oqnD+xViN0AMguOA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.25", + "@angular/core": "19.2.25", + "@angular/platform-browser": "19.2.25", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.7.tgz", + "integrity": "sha512-GDKuYHjP7vAI1kjBo73V+STKr9XIMZknW/xirpRW/EcShX0IKSev/ALafeRfC8Q331nodrXUFu04PugPB0MAhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.7", + "@jsonjoy.com/fs-node-utils": "4.57.7", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.7.tgz", + "integrity": "sha512-1rWsah2nZtRbNeP+c61QcfGfVrJXBmBD0Hm7Akvv4C9MKEasXnbiOS//iH3T3HwUSSBATGrfSp0Xi8nlNhATeQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.7", + "@jsonjoy.com/fs-node-builtins": "4.57.7", + "@jsonjoy.com/fs-node-utils": "4.57.7", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.7.tgz", + "integrity": "sha512-xhnyeyEVTiIOibFvda/5n89nChMLCPKHHM2WQ+GGDf6+U/IrQBW3Qx6x+Uq1bkDbxBkybLOdIGoBtVBrE8Nngg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.7", + "@jsonjoy.com/fs-node-builtins": "4.57.7", + "@jsonjoy.com/fs-node-utils": "4.57.7", + "@jsonjoy.com/fs-print": "4.57.7", + "@jsonjoy.com/fs-snapshot": "4.57.7", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.7.tgz", + "integrity": "sha512-LWqfY1m+uAosjwM1RrKhMkUnP9jcq1RUczHsNO779ovm1E9v8I/pmj04eBAcoBjhC7ltcPbNFGyRJ5JqSJ7Jdg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.7.tgz", + "integrity": "sha512-9T0zC9LKcAWXDoTLRdLMoJ0seOvJ5bgDKq1tSBoQAFQpPDstQUeV1Oe7PLypdu7F2D3ddRstmwgeNUEN/VaZ4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.57.7", + "@jsonjoy.com/fs-node-builtins": "4.57.7", + "@jsonjoy.com/fs-node-utils": "4.57.7" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.7.tgz", + "integrity": "sha512-jjWSDOsfcog2cZnUCwX5AHmlIq6b6wx5Pz/2LAcNjJ62Rajwg89Fy7ubN+lDHew0/1reLDa9Z5urybYadhh37g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.57.7" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.7.tgz", + "integrity": "sha512-mFM4P4Gjq0QQHkLnXzPYPEMFrAoe6a5Myedgb6+CmL+nGd3MKvTxYPuD7N1dLIH9RBy1fLdzxd80qvuK8xrx3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.57.7", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.7.tgz", + "integrity": "sha512-1GS3+plfm2giB3PqokiqyydyqYTPLcCQIKSkp0TdMNRh3KVk7rqRM6U785FLlVRG7XLmkc0KWr215OY+22K3QA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.57.7", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.6.tgz", + "integrity": "sha512-yF/ih9EJJZc72psFQbwnn8mExIWfTnzWJg+N02hnpXtDPETYLmQswIMBn7+V88lfCaFrMozJsUvcEQIkEPU0Gg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.6.tgz", + "integrity": "sha512-5BbCumsFLbCi586Bb1lTWQFkekdQUw8/t8cy++Uq251cl3hbDIGEwD9HAwh8H6IS2F6QA9KdKmO136LmipRNkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.6.tgz", + "integrity": "sha512-+6XgLpMb7HBoWxXj+bLbiiB4s0mRRcDPElnRS3LpWRzdYSe+gFk5MT/4RrVNqd2MESUDmb53NUXw1+BP69bjiQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.6.tgz", + "integrity": "sha512-l5VmJamJ3nyMmeD1ANBQCQqy7do1ESaJQfKPSm2IG9/ADZryptTyCj8N6QaYgIWewqNUrcbdMkJajRQAt5Qjfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.6.tgz", + "integrity": "sha512-nDYT8qN9si5+onHYYaI4DiauDMx24OAiuZAUsEqrDy+ja/3EbpXPX/VAkMV8AEaQhy3xc4dRC+KcYIvOFefJ4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.6.tgz", + "integrity": "sha512-XlqVtILonQnG+9fH2N3Aytria7P/1fwDgDhl29rde96uH2sLB8CHORIf2PfuLVzFQJ7Uqp8py9AYwr3ZUCFfWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngtools/webpack": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.27.tgz", + "integrity": "sha512-nuKxH4WaRILjlje4143T8XeDWVi5AIDlCMu/SPSKsYtyGxX7AC3sEu0UatFG7rrJkS/SB+fZngtuydoprhvYkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0 || ^19.2.0-next.0", + "typescript": ">=5.5 <5.9", + "webpack": "^5.54.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-18.0.0.tgz", + "integrity": "sha512-Z9u9AXaeuyeHHimivvJQvhal/LJNB+5tlH+FimSU4QQ61FrtRDqgyn6nfFbc6Cb+59NDZdsh4QeTJyPCldFUwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=18", + "@angular/core": ">=18", + "rxjs": ">=7" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-18.0.0.tgz", + "integrity": "sha512-p6QpNcU3rkCYjHbKVIadcYtml/Njpr0aLp8neJ4uJWQ4Xm9f09bIePhOjdjAFHupTptKqKrG1J2gSi3RSwgYeA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=18", + "@angular/core": ">=18", + "@ngx-translate/core": ">=18.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", + "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz", + "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "19.2.27", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.27.tgz", + "integrity": "sha512-kJX5nyDjwo6iHQ+g/AJa+4SyHRSQyGgutpqXUIuYY2htGWVS1HUQlHfwAER5btjSNKe/iHgh4pzYGZfvJ2V6/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.27", + "@angular-devkit/schematics": "19.2.27", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", + "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", + "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", + "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", + "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/beasties": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.2.tgz", + "integrity": "sha512-p4AF8uYzm9Fwu8m/hSVTCPXrRBPmB34hQpHsec2KOaR9CZmgoU8IOv4Cvwq4hgz2p4hLMNbsdNl5XeA6XbAQwA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.1.tgz", + "integrity": "sha512-9KM4QMPKnaJqaja1v7gYO/+TXZGLtzPA05NmUTqDAJjcsWeVoOXKMvU9g0gfuuoYTQqJZ924hivICd5R/bCJbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.375", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz", + "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.28.0.tgz", + "integrity": "sha512-5TRVKExcEmeMkccIZMzUq+Az6X2RoMAJyfl6SMMO1dMVhmvt0I2mx7gAb6zYi42n4d1ETcatFXazGKzA+aW7fg==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.6.0.tgz", + "integrity": "sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf/node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/less": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", + "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.6.tgz", + "integrity": "sha512-SuHqzPl7mYStna8WRotY8XX/EUZBjjv3QyKIByeCLFfC9uXT/OIHByEcA07PzbMfQAM0KYJtLgtpMRlIe5dErQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.6", + "@lmdb/lmdb-darwin-x64": "3.2.6", + "@lmdb/lmdb-linux-arm": "3.2.6", + "@lmdb/lmdb-linux-arm64": "3.2.6", + "@lmdb/lmdb-linux-x64": "3.2.6", + "@lmdb/lmdb-win32-x64": "3.2.6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.57.7", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.7.tgz", + "integrity": "sha512-YZPphUQZSRGk6ddPlsNuMbztrLwsbUATFNZcqKscSbSJZ4g0+Y3vSZLJ/rfnGZaB1FFhC7SrywZXev6i8lnHgg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.57.7", + "@jsonjoy.com/fs-fsa": "4.57.7", + "@jsonjoy.com/fs-node": "4.57.7", + "@jsonjoy.com/fs-node-builtins": "4.57.7", + "@jsonjoy.com/fs-node-to-fsa": "4.57.7", + "@jsonjoy.com/fs-node-utils": "4.57.7", + "@jsonjoy.com/fs-print": "4.57.7", + "@jsonjoy.com/fs-snapshot": "4.57.7", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz", + "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", + "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thingies": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", + "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", + "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", + "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz", + "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", + "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", + "license": "MIT" + } + } +} diff --git a/meal-plan-frontend-angular/package.json b/meal-plan-frontend-angular/package.json new file mode 100644 index 0000000..aabe48f --- /dev/null +++ b/meal-plan-frontend-angular/package.json @@ -0,0 +1,44 @@ +{ + "name": "meal-plan-frontend-angular", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/common": "^19.2.0", + "@angular/compiler": "^19.2.0", + "@angular/core": "^19.2.0", + "@angular/forms": "^19.2.0", + "@angular/platform-browser": "^19.2.0", + "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/router": "^19.2.0", + "@ngx-translate/core": "^18.0.0", + "@ngx-translate/http-loader": "^18.0.0", + "html2canvas": "^1.4.1", + "jspdf": "^4.2.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.2.27", + "@angular/cli": "^19.2.27", + "@angular/compiler-cli": "^19.2.0", + "@types/jasmine": "~5.1.0", + "autoprefixer": "^10.5.0", + "jasmine-core": "~5.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.14", + "typescript": "~5.7.2" + } +} diff --git a/meal-plan-frontend-angular/postcss.config.js b/meal-plan-frontend-angular/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/meal-plan-frontend-angular/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/meal-plan-frontend-angular/proxy.conf.json b/meal-plan-frontend-angular/proxy.conf.json new file mode 100644 index 0000000..e4a6ace --- /dev/null +++ b/meal-plan-frontend-angular/proxy.conf.json @@ -0,0 +1,7 @@ +{ + "/api": { + "target": "http://localhost:5000", + "secure": false, + "changeOrigin": true + } +} diff --git a/meal-plan-frontend-angular/public/favicon.ico b/meal-plan-frontend-angular/public/favicon.ico new file mode 100644 index 0000000..57614f9 Binary files /dev/null and b/meal-plan-frontend-angular/public/favicon.ico differ diff --git a/meal-plan-frontend-angular/public/favicon.svg b/meal-plan-frontend-angular/public/favicon.svg new file mode 100644 index 0000000..e092d86 --- /dev/null +++ b/meal-plan-frontend-angular/public/favicon.svg @@ -0,0 +1,7 @@ + + + + diff --git a/meal-plan-frontend-angular/public/i18n/en.json b/meal-plan-frontend-angular/public/i18n/en.json new file mode 100644 index 0000000..0c5929a --- /dev/null +++ b/meal-plan-frontend-angular/public/i18n/en.json @@ -0,0 +1,434 @@ +{ + "app": { + "name": "DailyMeals" + }, + "language": { + "label": "Language", + "en": "English", + "pl": "Polish" + }, + "layout": { + "label": "Layout", + "sidebar": "Sidebar", + "topnav": "Top navigation", + "compact": "Compact sidebar", + "wide": "Wide sidebar", + "wideTagline": "Plan your meals with ease" + }, + "appearance": { + "label": "Appearance", + "forest": "Forest Classic", + "forestDesc": "Green palette, outline icons", + "ocean": "Ocean Fresh", + "oceanDesc": "Blue palette, outline icons", + "sunset": "Sunset Kitchen", + "sunsetDesc": "Warm amber palette, emoji icons", + "berry": "Berry Modern", + "berryDesc": "Violet palette, bold icons" + }, + "nav": { + "dashboard": "Dashboard", + "breakfast": "Breakfast", + "secondBreakfast": "Second Breakfast", + "lunch": "Lunch", + "dinner": "Dinner", + "allMeals": "All Meals", + "manageMeals": "Manage Meals", + "mealPlanner": "Meal Planner", + "shoppingList": "Shopping List", + "dietTracker": "My Diet", + "ingredientCatalog": "Ingredients", + "dietGenerator": "Diet Generator", + "recipeGenerator": "Recipe Generator", + "logout": "Logout", + "user": "User", + "toggleNav": "Toggle navigation", + "toggleTheme": "Toggle theme", + "lightMode": "Switch to light mode", + "darkMode": "Switch to dark mode" + }, + "categories": { + "breakfast": "Breakfast", + "secondBreakfast": "Second Breakfast", + "lunch": "Lunch", + "dinner": "Dinner", + "unknown": "Unknown" + }, + "common": { + "loading": "Loading...", + "tryAgain": "Try again", + "back": "Back", + "add": "Add", + "category": "Category", + "all": "All", + "search": "Search", + "save": "Save", + "cancel": "Cancel", + "edit": "Edit", + "delete": "Delete", + "optional": "optional", + "recipeCount_one": "{{count}} recipe", + "recipeCount_other": "{{count}} recipes", + "selectedCount_one": "{{count}} selected", + "selectedCount_other": "{{count}} selected" + }, + "auth": { + "signInSubtitle": "Sign in to plan your meals", + "email": "Email", + "password": "Password", + "emailPlaceholder": "you@example.com", + "signIn": "Sign in", + "signingIn": "Signing in...", + "noAccount": "Don't have an account?", + "createOne": "Create one", + "createAccountTitle": "Create account", + "createAccountSubtitle": "Join DailyMeals to browse and plan meals", + "displayName": "Display name", + "displayNamePlaceholder": "Piotr", + "confirmPassword": "Confirm password", + "passwordHint": "At least 8 characters with uppercase, lowercase, and a number.", + "createAccount": "Create account", + "creatingAccount": "Creating account...", + "hasAccount": "Already have an account?" + }, + "validation": { + "emailRequired": "Email is required.", + "emailInvalid": "Enter a valid email address.", + "passwordRequired": "Password is required.", + "displayNameTooLong": "Display name is too long.", + "passwordMinLength": "Password must be at least 8 characters.", + "passwordUppercase": "Include at least one uppercase letter.", + "passwordLowercase": "Include at least one lowercase letter.", + "passwordDigit": "Include at least one digit.", + "confirmPasswordRequired": "Please confirm your password.", + "passwordsMismatch": "Passwords do not match.", + "recipeNameRequired": "Recipe name is required.", + "ingredientNameRequired": "Ingredient name is required.", + "stepNumberMin": "Step number must be at least 1.", + "stepDescriptionRequired": "Step description is required.", + "stepsMin": "Add at least one preparation step." + }, + "errors": { + "generic": "Something went wrong.", + "signInFailed": "Unable to sign in.", + "registerFailed": "Unable to create account.", + "loadCategoriesFailed": "Failed to load categories.", + "loadRecipesFailed": "Failed to load recipes.", + "loadRecipeFailed": "Failed to load recipe.", + "loadDietFailed": "Failed to load diet data.", + "loadMealPlanFailed": "Failed to load meal plan.", + "loadShoppingListFailed": "Failed to load shopping list.", + "loadIngredientCatalogFailed": "Failed to load ingredient catalog.", + "saveIngredientFailed": "Failed to save ingredient.", + "saveRecipeFailed": "Failed to save recipe.", + "recalculateMacrosFailed": "Failed to recalculate recipe macros.", + "importFailed": "Import failed.", + "pdfLoadFailed": "Could not load recipes for export.", + "pdfGenerateFailed": "PDF generation failed.", + "pdfRenderAreaMissing": "PDF render area not found.", + "pdfNothingToExport": "Nothing to export." + }, + "dashboard": { + "welcome": "Welcome back", + "welcomeNamed": "Welcome back, {{name}}", + "subtitle": "Browse recipes by meal category or explore everything at once.", + "viewRecipes": "View recipes" + }, + "recipes": { + "allMealsTitle": "All Meals", + "allMealsSubtitle": "Select recipes and export them with a combined shopping list.", + "selectAllVisible": "Select all visible", + "deselectAll": "Deselect all", + "searchRecipes": "Search recipes...", + "searchByIngredient": "Search by ingredient...", + "searchByIngredientLabel": "Search by ingredient", + "searchByNameAria": "Search recipes by name", + "searchByIngredientAria": "Search recipes by ingredient", + "noMatchingTitle": "No matching recipes", + "noMatchingIngredient": "No recipes contain that ingredient. Try a different ingredient name.", + "noMatchingFilter": "Try adjusting your search or category filter.", + "emptyCategoryTitle": "No {{category}} recipes yet", + "emptyCategoryDescription": "There are currently no recipes in this category.", + "selectRecipe": "Select {{name}}", + "prepTime": "{{count}} min preparation", + "calories": "Calories", + "protein": "Protein", + "fat": "Fat", + "carbs": "Carbs", + "ingredients": "Ingredients", + "preparation": "Preparation", + "noIngredients": "No ingredients listed.", + "noSteps": "No steps listed.", + "kcal": "{{count}} kcal", + "minutes": "{{count}} min" + }, + "manage": { + "title": "Manage Meals", + "subtitle": "Add recipes manually or import them from an Excel spreadsheet.", + "tabManual": "Add manually", + "tabEdit": "Edit recipes", + "tabImport": "Import Excel", + "savedSuccess": "\"{{name}}\" saved successfully.", + "basicInfo": "Basic info", + "recipeName": "Recipe name", + "recipeNamePlaceholder": "Grilled chicken salad", + "prepTimeMin": "Prep time (min)", + "caloriesKcal": "Calories (kcal)", + "proteinG": "Protein (g)", + "fatG": "Fat (g)", + "carbsG": "Carbs (g)", + "preparationSteps": "Preparation steps", + "addStep": "Add step", + "stepPlaceholder": "Describe this step...", + "ingredientName": "Name", + "ingredientGrams": "Grams", + "ingredientUnit": "Unit", + "removeIngredient": "Remove ingredient", + "removeStep": "Remove step", + "saveRecipe": "Save recipe", + "updateRecipe": "Update recipe", + "updatedSuccess": "\"{{name}}\" updated successfully.", + "selectRecipeToEdit": "Select a recipe from the list to edit.", + "catalogLink": "Link to ingredient catalog", + "estimatedFromCatalog": "Estimated from linked catalog items", + "unlinkedIngredientsWarning": "{{count}} ingredient(s) have grams but no catalog link — macros may be incomplete.", + "recalculateMacros": "Recalculate all macros", + "recalculating": "Recalculating…", + "recalculateSuccess": "Updated {{updated}} of {{processed}} recipes. {{unlinked}} ingredient row(s) still unlinked.", + "macroHint": "Estimated: {{calories}} kcal · P {{protein}}g · F {{fat}}g · C {{carbs}}g", + "saving": "Saving...", + "importTitle": "Import from Excel", + "importDescription": "Use the template with three sheets: Recipes, Ingredients, and Steps. Recipe names must match across sheets.", + "downloadTemplate": "Download template", + "uploadLabel": "Upload filled .xlsx file", + "importComplete": "Import complete", + "createdCount": "Created: {{count}}", + "skippedCount": "Skipped: {{count}}", + "importRecipes": "Import recipes", + "importing": "Importing...", + "excelFormatTitle": "Excel format", + "excelRecipesSheet": "Recipes: Name, MealCategory (Breakfast / Lunch / 0–3), Calories, Protein, Fat, Carbs, PrepTimeMinutes", + "excelIngredientsSheet": "Ingredients: RecipeName, Name, AmountGrams, Unit, SortOrder", + "excelStepsSheet": "Steps: RecipeName, StepNumber, Description", + "duplicateSkipped": "Duplicate recipe names are skipped." + }, + "generators": { + "diet": { + "title": "Diet Generator", + "subtitle": "Calculate targets from your profile, build a weekly plan from your recipe catalog, and save it to the meal planner.", + "profileTitle": "Your profile", + "heightCm": "Height (cm)", + "weightKg": "Weight (kg)", + "age": "Age", + "sex": "Sex", + "male": "Male", + "female": "Female", + "activity": "Activity level", + "sedentary": "Sedentary", + "light": "Light", + "moderate": "Moderate", + "active": "Active", + "veryActive": "Very active", + "goal": "Goal", + "loseWeight": "Lose weight", + "maintain": "Maintain", + "gainMuscle": "Gain muscle", + "recomp": "Recomposition", + "allergies": "Allergies / notes", + "continue": "Calculate targets", + "macrosTitle": "Daily targets", + "acceptAndGenerate": "Accept & generate 7-day plan", + "planHint": "Each day should be within ±10 kcal of your target. Lock meals you like, regenerate others.", + "lock": "Lock", + "unlock": "Unlock", + "viewRecipe": "Details", + "noAlternativeMeals": "No other recipes are available for this meal — all suitable options are already in your plan.", + "commit": "Save plan to calendar", + "done": "Plan saved to your meal planner and daily goals.", + "newPlan": "Create another plan", + "error": "Diet generator failed." + }, + "recipe": { + "title": "Recipe Generator", + "subtitle": "Create several recipe drafts with AI, review them, and save the ones you like.", + "constraintsTitle": "What should we cook?", + "prompt": "Describe the meal", + "promptPlaceholder": "Light breakfast with oatmeal and fruit…", + "targetCalories": "Target calories (kcal)", + "targetCaloriesOptional": "e.g. 500", + "targetCaloriesHint": "Ingredient amounts will be scaled to match (±10 kcal).", + "caloriesWithTarget": "{{actual}} kcal (target: {{target}})", + "maxPrep": "Max prep time (min)", + "exclusions": "Exclude ingredients", + "generate": "Generate recipe", + "recipeCount": "How many recipes to generate", + "reviewHint": "Review {{count}} generated recipes. Uncheck or remove ones you don't want, then save the rest.", + "selectAll": "Select all", + "remove": "Remove", + "saveSelected": "Save selected ({{count}})", + "doneMultiple": "{{count}} recipes saved to your library.", + "generateMore": "Generate more recipes", + "regenIngredients": "Regenerate ingredients", + "regenSteps": "Regenerate steps", + "regenFull": "Regenerate all", + "save": "Save to library", + "done": "Recipe saved.", + "viewRecipe": "View recipe", + "error": "Recipe generator failed." + } + }, + "pdf": { + "generate": "Generate PDF", + "preparing": "Preparing...", + "generating": "Generating...", + "prepTime": "Prep time: {{count}} min", + "shoppingList": "Shopping List", + "generatedOn_one": "Generated {{date}} · {{count}} recipe", + "generatedOn_other": "Generated {{date}} · {{count}} recipes", + "recipesInExport": "Recipes in this export", + "toTaste": "to taste", + "dayPlanTitle": "Daily meal plan — {{date}}", + "emptySlot": "Not planned", + "emptyShoppingList": "No shopping items for this day.", + "plannedMealMeta": "{{slot}} · {{portions}}× portions", + "dayShoppingSubtitle": "List for {{date}} · {{count}} meals" + }, + "diet": { + "title": "My Diet", + "subtitle": "Track what you eat and drink, set daily targets, and stay on top of reminders.", + "tabToday": "Today", + "tabHistory": "History", + "tabGoals": "Goals", + "tabReminders": "Reminders", + "goToday": "Today", + "notToday": "not today", + "quickLog": "Quick log", + "copyYesterday": "Copy yesterday's meals", + "recentMeals": "Recent", + "favorites": "Favorites", + "days": "days", + "historyCalories": "Calories over time", + "historyMax": "Peak: {{max}} kcal", + "noHistory": "No history for this period.", + "mealsOnDate": "Meals on {{date}}", + "drinksOnDate": "Drinks on {{date}}", + "editReminder": "Edit reminder", + "calories": "Calories", + "water": "Water", + "snack": "Snack", + "logDrink": "Log a drink", + "logMeal": "Mark a meal as eaten", + "searchRecipe": "From a recipe", + "orCustomMeal": "Or enter a custom meal name", + "markEaten": "Mark as eaten", + "mealsToday": "Meals today", + "drinksToday": "Drinks today", + "nothingLogged": "Nothing logged yet.", + "noMacros": "No nutrition info", + "removeEntry": "Remove entry", + "dailyGoals": "Daily goals", + "goalsHint": "Set targets like Lifesum or MyFitnessPal — the Today tab shows what is left.", + "calorieGoal": "Calorie goal (kcal)", + "waterGoal": "Water goal (ml)", + "saveGoals": "Save goals", + "addReminder": "Add reminder", + "reminderType": "Type", + "reminderWater": "Drink water", + "reminderMeal": "Meal time", + "reminderCustom": "Custom", + "reminderTitle": "Title", + "reminderTitlePlaceholder": "Drink a glass of water", + "reminderTime": "Time", + "saveReminder": "Save reminder", + "yourReminders": "Your reminders", + "noReminders": "No reminders yet.", + "remaining": "Left", + "portions": "Portions", + "fromCatalog": "From ingredient catalog", + "noCatalogItem": "None", + "grams": "Amount (g)", + "mealPreview": "This meal adds", + "remainingAfter": "After logging you will still have", + "suggestionsTitle": "What to eat next", + "suggestionsHint": "Based on what you still need today.", + "logSuggestion": "Log", + "suggestionReasons": { + "highProtein": "High protein", + "needCarbs": "Good carbs", + "needFat": "Healthy fats", + "needCalories": "Energy boost", + "balanced": "Balanced choice" + } + }, + "mealPlanner": { + "title": "Meal Planner", + "subtitle": "Plan recipes for each day of the week.", + "today": "This week", + "addRecipe": "Add recipe", + "dailyTarget": "Daily calorie target", + "daySummary": "{{logged}} / {{target}} kcal planned", + "remaining": "{{count}} kcal left", + "slotHint": "Suggested ~{{count}} kcal for this meal", + "suggestionsTitle": "Suggestions for this slot", + "noSuggestions": "Set a daily calorie target to see suggestions.", + "estimatedMeal": "This meal: ~{{count}} kcal", + "selectedRecipe": "Selected: {{name}}", + "clearSelection": "Clear selection", + "exportDay": "Export day plan (PDF)", + "includingSelection": "incl. current pick: {{count}} kcal" + }, + "shoppingList": { + "title": "Shopping List", + "subtitle": "Aggregated ingredients from your meal plan.", + "from": "From", + "to": "To", + "load": "Load list", + "clearChecks": "Clear checked items", + "empty": "No items for this date range.", + "checklistHint": "Checked items are saved locally in your browser." + }, + "ingredientCatalog": { + "title": "Ingredient catalog", + "subtitle": "Nutrition values per 100 g, grouped by food category.", + "allCategories": "All", + "searchPlaceholder": "Search ingredients…", + "noResultsTitle": "No ingredients found", + "noResultsDescription": "Try another category or search term.", + "per100gNote": "All values are per 100 g.", + "addIngredient": "Add ingredient", + "editIngredient": "Edit ingredient", + "selectCategory": "Select category", + "nameEn": "Name (English)", + "namePlLabel": "Name (Polish)", + "formInvalid": "Please fill in category and name.", + "deleteConfirm": "Remove this ingredient from the catalog?", + "columns": { + "name": "Ingredient", + "category": "Category", + "calories": "kcal", + "protein": "Protein (g)", + "fat": "Fat (g)", + "carbs": "Carbs (g)", + "fiber": "Fiber (g)" + }, + "categories": { + "meat": "Meat", + "poultry": "Poultry", + "fish": "Fish", + "seafood": "Seafood", + "vegetables": "Vegetables", + "fruits": "Fruits", + "dairy": "Dairy", + "eggs": "Eggs", + "grains": "Grains", + "legumes": "Legumes", + "nuts": "Nuts & seeds", + "oils": "Oils & fats" + } + }, + "notFound": { + "title": "Page not found", + "description": "The page you're looking for doesn't exist.", + "backToDashboard": "Back to dashboard" + } +} diff --git a/meal-plan-frontend-angular/public/i18n/pl.json b/meal-plan-frontend-angular/public/i18n/pl.json new file mode 100644 index 0000000..0351f65 --- /dev/null +++ b/meal-plan-frontend-angular/public/i18n/pl.json @@ -0,0 +1,440 @@ +{ + "app": { + "name": "DailyMeals" + }, + "language": { + "label": "Język", + "en": "Angielski", + "pl": "Polski" + }, + "layout": { + "label": "Układ", + "sidebar": "Panel boczny", + "topnav": "Nawigacja górna", + "compact": "Wąski panel", + "wide": "Szeroki panel", + "wideTagline": "Planuj posiłki wygodnie" + }, + "appearance": { + "label": "Wygląd", + "forest": "Leśny klasyczny", + "forestDesc": "Zielona paleta, ikony konturowe", + "ocean": "Oceaniczna świeżość", + "oceanDesc": "Niebieska paleta, ikony konturowe", + "sunset": "Zachód kuchni", + "sunsetDesc": "Ciepła bursztynowa paleta, emoji", + "berry": "Jagodowy modern", + "berryDesc": "Fioletowa paleta, pogrubione ikony" + }, + "nav": { + "dashboard": "Panel", + "breakfast": "Śniadanie", + "secondBreakfast": "Drugie śniadanie", + "lunch": "Obiad", + "dinner": "Kolacja", + "allMeals": "Wszystkie posiłki", + "manageMeals": "Zarządzaj posiłkami", + "mealPlanner": "Plan posiłków", + "shoppingList": "Lista zakupów", + "dietTracker": "Moja dieta", + "ingredientCatalog": "Składniki", + "dietGenerator": "Generator diety", + "recipeGenerator": "Generator przepisów", + "logout": "Wyloguj", + "user": "Użytkownik", + "toggleNav": "Przełącz nawigację", + "toggleTheme": "Przełącz motyw", + "lightMode": "Przełącz na jasny motyw", + "darkMode": "Przełącz na ciemny motyw" + }, + "categories": { + "breakfast": "Śniadanie", + "secondBreakfast": "Drugie śniadanie", + "lunch": "Obiad", + "dinner": "Kolacja", + "unknown": "Nieznana" + }, + "common": { + "loading": "Ładowanie...", + "tryAgain": "Spróbuj ponownie", + "back": "Wstecz", + "add": "Dodaj", + "category": "Kategoria", + "all": "Wszystkie", + "search": "Szukaj", + "save": "Zapisz", + "cancel": "Anuluj", + "edit": "Edytuj", + "delete": "Usuń", + "optional": "opcjonalnie", + "recipeCount_one": "{{count}} przepis", + "recipeCount_few": "{{count}} przepisy", + "recipeCount_many": "{{count}} przepisów", + "recipeCount_other": "{{count}} przepisów", + "selectedCount_one": "Wybrano: {{count}}", + "selectedCount_few": "Wybrano: {{count}}", + "selectedCount_many": "Wybrano: {{count}}", + "selectedCount_other": "Wybrano: {{count}}" + }, + "auth": { + "signInSubtitle": "Zaloguj się, aby planować posiłki", + "email": "E-mail", + "password": "Hasło", + "emailPlaceholder": "ty@example.com", + "signIn": "Zaloguj się", + "signingIn": "Logowanie...", + "noAccount": "Nie masz konta?", + "createOne": "Utwórz je", + "createAccountTitle": "Utwórz konto", + "createAccountSubtitle": "Dołącz do DailyMeals, aby przeglądać i planować posiłki", + "displayName": "Nazwa wyświetlana", + "displayNamePlaceholder": "Piotr", + "confirmPassword": "Potwierdź hasło", + "passwordHint": "Co najmniej 8 znaków, w tym wielka litera, mała litera i cyfra.", + "createAccount": "Utwórz konto", + "creatingAccount": "Tworzenie konta...", + "hasAccount": "Masz już konto?" + }, + "validation": { + "emailRequired": "E-mail jest wymagany.", + "emailInvalid": "Podaj prawidłowy adres e-mail.", + "passwordRequired": "Hasło jest wymagane.", + "displayNameTooLong": "Nazwa wyświetlana jest zbyt długa.", + "passwordMinLength": "Hasło musi mieć co najmniej 8 znaków.", + "passwordUppercase": "Dołącz co najmniej jedną wielką literę.", + "passwordLowercase": "Dołącz co najmniej jedną małą literę.", + "passwordDigit": "Dołącz co najmniej jedną cyfrę.", + "confirmPasswordRequired": "Potwierdź hasło.", + "passwordsMismatch": "Hasła nie są identyczne.", + "recipeNameRequired": "Nazwa przepisu jest wymagana.", + "ingredientNameRequired": "Nazwa składnika jest wymagana.", + "stepNumberMin": "Numer kroku musi być co najmniej 1.", + "stepDescriptionRequired": "Opis kroku jest wymagany.", + "stepsMin": "Dodaj co najmniej jeden krok przygotowania." + }, + "errors": { + "generic": "Coś poszło nie tak.", + "signInFailed": "Nie udało się zalogować.", + "registerFailed": "Nie udało się utworzyć konta.", + "loadCategoriesFailed": "Nie udało się załadować kategorii.", + "loadRecipesFailed": "Nie udało się załadować przepisów.", + "loadRecipeFailed": "Nie udało się załadować przepisu.", + "loadDietFailed": "Nie udało się załadować danych diety.", + "loadMealPlanFailed": "Nie udało się załadować planu posiłków.", + "loadShoppingListFailed": "Nie udało się załadować listy zakupów.", + "loadIngredientCatalogFailed": "Nie udało się załadować katalogu składników.", + "saveIngredientFailed": "Nie udało się zapisać składnika.", + "saveRecipeFailed": "Nie udało się zapisać przepisu.", + "recalculateMacrosFailed": "Nie udało się przeliczyć makroskładników przepisów.", + "importFailed": "Import nie powiódł się.", + "pdfLoadFailed": "Nie udało się załadować przepisów do eksportu.", + "pdfGenerateFailed": "Generowanie PDF nie powiodło się.", + "pdfRenderAreaMissing": "Nie znaleziono obszaru renderowania PDF.", + "pdfNothingToExport": "Brak danych do eksportu." + }, + "dashboard": { + "welcome": "Witaj ponownie", + "welcomeNamed": "Witaj ponownie, {{name}}", + "subtitle": "Przeglądaj przepisy według kategorii posiłków lub odkrywaj wszystkie naraz.", + "viewRecipes": "Zobacz przepisy" + }, + "recipes": { + "allMealsTitle": "Wszystkie posiłki", + "allMealsSubtitle": "Wybierz przepisy i wyeksportuj je wraz ze wspólną listą zakupów.", + "selectAllVisible": "Zaznacz widoczne", + "deselectAll": "Odznacz wszystkie", + "searchRecipes": "Szukaj przepisów...", + "searchByIngredient": "Szukaj po składniku...", + "searchByIngredientLabel": "Szukaj po składniku", + "searchByNameAria": "Szukaj przepisów po nazwie", + "searchByIngredientAria": "Szukaj przepisów po składniku", + "noMatchingTitle": "Brak pasujących przepisów", + "noMatchingIngredient": "Żaden przepis nie zawiera tego składnika. Spróbuj innej nazwy.", + "noMatchingFilter": "Spróbuj zmienić wyszukiwanie lub filtr kategorii.", + "emptyCategoryTitle": "Brak przepisów: {{category}}", + "emptyCategoryDescription": "W tej kategorii nie ma jeszcze żadnych przepisów.", + "selectRecipe": "Wybierz {{name}}", + "prepTime": "{{count}} min przygotowania", + "calories": "Kalorie", + "protein": "Białko", + "fat": "Tłuszcz", + "carbs": "Węglowodany", + "ingredients": "Składniki", + "preparation": "Przygotowanie", + "noIngredients": "Brak składników.", + "noSteps": "Brak kroków.", + "kcal": "{{count}} kcal", + "minutes": "{{count}} min" + }, + "manage": { + "title": "Zarządzaj posiłkami", + "subtitle": "Dodawaj przepisy ręcznie lub importuj je z arkusza Excel.", + "tabManual": "Dodaj ręcznie", + "tabEdit": "Edytuj przepisy", + "tabImport": "Import Excel", + "savedSuccess": "„{{name}}” zapisano pomyślnie.", + "basicInfo": "Podstawowe informacje", + "recipeName": "Nazwa przepisu", + "recipeNamePlaceholder": "Sałatka z grillowanym kurczakiem", + "prepTimeMin": "Czas przygotowania (min)", + "caloriesKcal": "Kalorie (kcal)", + "proteinG": "Białko (g)", + "fatG": "Tłuszcz (g)", + "carbsG": "Węglowodany (g)", + "preparationSteps": "Kroki przygotowania", + "addStep": "Dodaj krok", + "stepPlaceholder": "Opisz ten krok...", + "ingredientName": "Nazwa", + "ingredientGrams": "Gramy", + "ingredientUnit": "Jednostka", + "removeIngredient": "Usuń składnik", + "removeStep": "Usuń krok", + "saveRecipe": "Zapisz przepis", + "updateRecipe": "Aktualizuj przepis", + "updatedSuccess": "„{{name}}” zaktualizowano pomyślnie.", + "selectRecipeToEdit": "Wybierz przepis z listy, aby go edytować.", + "catalogLink": "Powiąż z katalogiem składników", + "estimatedFromCatalog": "Szacunkowo z powiązanych składników", + "unlinkedIngredientsWarning": "{{count}} składnik(ów) ma gramy, ale brak powiązania z katalogiem — makra mogą być niepełne.", + "recalculateMacros": "Przelicz wszystkie makra", + "recalculating": "Przeliczanie…", + "recalculateSuccess": "Zaktualizowano {{updated}} z {{processed}} przepisów. {{unlinked}} wierszy składników nadal niepowiązanych.", + "macroHint": "Szacunkowo: {{calories}} kcal · B {{protein}}g · T {{fat}}g · W {{carbs}}g", + "saving": "Zapisywanie...", + "importTitle": "Import z Excela", + "importDescription": "Użyj szablonu z trzema arkuszami: Recipes, Ingredients i Steps. Nazwy przepisów muszą się zgadzać między arkuszami.", + "downloadTemplate": "Pobierz szablon", + "uploadLabel": "Prześlij wypełniony plik .xlsx", + "importComplete": "Import zakończony", + "createdCount": "Utworzono: {{count}}", + "skippedCount": "Pominięto: {{count}}", + "importRecipes": "Importuj przepisy", + "importing": "Importowanie...", + "excelFormatTitle": "Format Excela", + "excelRecipesSheet": "Recipes: Name, MealCategory (Breakfast / Lunch / 0–3), Calories, Protein, Fat, Carbs, PrepTimeMinutes", + "excelIngredientsSheet": "Ingredients: RecipeName, Name, AmountGrams, Unit, SortOrder", + "excelStepsSheet": "Steps: RecipeName, StepNumber, Description", + "duplicateSkipped": "Duplikaty nazw przepisów są pomijane." + }, + "generators": { + "diet": { + "title": "Generator diety", + "subtitle": "Oblicz cele z profilu, zbuduj tygodniowy plan z katalogu przepisów i zapisz w planerze.", + "profileTitle": "Twój profil", + "heightCm": "Wzrost (cm)", + "weightKg": "Waga (kg)", + "age": "Wiek", + "sex": "Płeć", + "male": "Mężczyzna", + "female": "Kobieta", + "activity": "Poziom aktywności", + "sedentary": "Siedzący", + "light": "Lekka", + "moderate": "Umiarkowana", + "active": "Aktywna", + "veryActive": "Bardzo aktywna", + "goal": "Cel", + "loseWeight": "Redukcja wagi", + "maintain": "Utrzymanie", + "gainMuscle": "Budowa mięśni", + "recomp": "Rekompozycja", + "allergies": "Alergie / uwagi", + "continue": "Oblicz cele", + "macrosTitle": "Dzienne cele", + "acceptAndGenerate": "Akceptuj i generuj plan 7 dni", + "planHint": "Każdy dzień powinien mieścić się w ±10 kcal od celu. Zablokuj lub wygeneruj ponownie posiłki.", + "lock": "Zablokuj", + "unlock": "Odblokuj", + "viewRecipe": "Szczegóły", + "noAlternativeMeals": "Brak innych przepisów na ten posiłek — wszystkie pasujące opcje są już w planie.", + "commit": "Zapisz plan w kalendarzu", + "done": "Plan zapisany w planerze i celach dziennych.", + "newPlan": "Utwórz kolejny plan", + "error": "Generator diety nie powiódł się." + }, + "recipe": { + "title": "Generator przepisów", + "subtitle": "Wygeneruj kilka propozycji przepisów z AI, przejrzyj je i zapisz te, które Ci pasują.", + "constraintsTitle": "Co ugotować?", + "prompt": "Opisz posiłek", + "promptPlaceholder": "Lekkie śniadanie z owsianką i owocami…", + "targetCalories": "Docelowa kaloryczność (kcal)", + "targetCaloriesOptional": "np. 500", + "targetCaloriesHint": "Składniki zostaną dopasowane do tej wartości (±10 kcal).", + "caloriesWithTarget": "{{actual}} kcal (cel: {{target}})", + "maxPrep": "Maks. czas przygotowania (min)", + "exclusions": "Wyklucz składniki", + "generate": "Generuj przepis", + "recipeCount": "Ile przepisów wygenerować", + "reviewHint": "Przejrzyj {{count}} wygenerowanych przepisów. Odznacz lub usuń te, których nie chcesz, i zapisz resztę.", + "selectAll": "Zaznacz wszystkie", + "remove": "Usuń", + "saveSelected": "Zapisz zaznaczone ({{count}})", + "doneMultiple": "Zapisano {{count}} przepisów w bibliotece.", + "generateMore": "Generuj kolejne przepisy", + "regenIngredients": "Generuj składniki ponownie", + "regenSteps": "Generuj kroki ponownie", + "regenFull": "Generuj całość ponownie", + "save": "Zapisz w bibliotece", + "done": "Przepis zapisany.", + "viewRecipe": "Zobacz przepis", + "error": "Generator przepisów nie powiódł się." + } + }, + "pdf": { + "generate": "Generuj PDF", + "preparing": "Przygotowywanie...", + "generating": "Generowanie...", + "prepTime": "Czas przygotowania: {{count}} min", + "shoppingList": "Lista zakupów", + "generatedOn_one": "Wygenerowano {{date}} · {{count}} przepis", + "generatedOn_few": "Wygenerowano {{date}} · {{count}} przepisy", + "generatedOn_many": "Wygenerowano {{date}} · {{count}} przepisów", + "generatedOn_other": "Wygenerowano {{date}} · {{count}} przepisów", + "recipesInExport": "Przepisy w tym eksporcie", + "toTaste": "do smaku", + "dayPlanTitle": "Plan posiłków — {{date}}", + "emptySlot": "Nie zaplanowano", + "emptyShoppingList": "Brak składników do zakupu dla tego dnia.", + "plannedMealMeta": "{{slot}} · {{portions}}× porcji", + "dayShoppingSubtitle": "Lista na {{date}} · {{count}} posiłków" + }, + "diet": { + "title": "Moja dieta", + "subtitle": "Śledź posiłki i napoje, ustaw cele dzienne i przypomnienia.", + "tabToday": "Dziś", + "tabHistory": "Historia", + "tabGoals": "Cele", + "tabReminders": "Przypomnienia", + "goToday": "Dziś", + "notToday": "inny dzień", + "quickLog": "Szybkie logowanie", + "copyYesterday": "Skopiuj wczorajsze posiłki", + "recentMeals": "Ostatnie", + "favorites": "Ulubione", + "days": "dni", + "historyCalories": "Kalorie w czasie", + "historyMax": "Szczyt: {{max}} kcal", + "noHistory": "Brak historii w tym okresie.", + "mealsOnDate": "Posiłki {{date}}", + "drinksOnDate": "Napoje {{date}}", + "editReminder": "Edytuj przypomnienie", + "calories": "Kalorie", + "water": "Woda", + "snack": "Przekąska", + "logDrink": "Dodaj napój", + "logMeal": "Oznacz zjedzony posiłek", + "searchRecipe": "Z przepisu", + "orCustomMeal": "Lub wpisz własną nazwę posiłku", + "markEaten": "Oznacz jako zjedzone", + "mealsToday": "Posiłki dziś", + "drinksToday": "Napoje dziś", + "nothingLogged": "Nic jeszcze nie zalogowano.", + "noMacros": "Brak danych odżywczych", + "removeEntry": "Usuń wpis", + "dailyGoals": "Cele dzienne", + "goalsHint": "Ustaw cele jak w Lifesum lub MyFitnessPal — zakładka Dziś pokaże, co zostało.", + "calorieGoal": "Cel kalorii (kcal)", + "waterGoal": "Cel wody (ml)", + "saveGoals": "Zapisz cele", + "addReminder": "Dodaj przypomnienie", + "reminderType": "Typ", + "reminderWater": "Pij wodę", + "reminderMeal": "Pora posiłku", + "reminderCustom": "Własne", + "reminderTitle": "Tytuł", + "reminderTitlePlaceholder": "Wypij szklankę wody", + "reminderTime": "Godzina", + "saveReminder": "Zapisz przypomnienie", + "yourReminders": "Twoje przypomnienia", + "noReminders": "Brak przypomnień.", + "remaining": "Pozostało", + "portions": "Porcje", + "fromCatalog": "Z katalogu składników", + "noCatalogItem": "Brak", + "grams": "Ilość (g)", + "mealPreview": "Ten posiłek dodaje", + "remainingAfter": "Po zapisaniu zostanie Ci", + "suggestionsTitle": "Co warto zjeść dalej", + "suggestionsHint": "Na podstawie tego, czego jeszcze brakuje dziś.", + "logSuggestion": "Zapisz", + "suggestionReasons": { + "highProtein": "Dużo białka", + "needCarbs": "Dobre węglowodany", + "needFat": "Zdrowe tłuszcze", + "needCalories": "Energia", + "balanced": "Zbilansowany wybór" + } + }, + "mealPlanner": { + "title": "Plan posiłków", + "subtitle": "Planuj przepisy na każdy dzień tygodnia.", + "today": "Ten tydzień", + "addRecipe": "Dodaj przepis", + "dailyTarget": "Cel kalorii na dzień", + "daySummary": "Zaplanowano {{logged}} / {{target}} kcal", + "remaining": "Pozostało {{count}} kcal", + "slotHint": "Sugerowane ~{{count}} kcal na ten posiłek", + "suggestionsTitle": "Propozycje na ten posiłek", + "noSuggestions": "Ustaw cel kalorii na dzień, aby zobaczyć propozycje.", + "estimatedMeal": "Ten posiłek: ~{{count}} kcal", + "selectedRecipe": "Wybrany: {{name}}", + "clearSelection": "Wyczyść wybór", + "exportDay": "Eksportuj plan dnia (PDF)", + "includingSelection": "w tym bieżący wybór: {{count}} kcal" + }, + "shoppingList": { + "title": "Lista zakupów", + "subtitle": "Składniki zebrane z planu posiłków.", + "from": "Od", + "to": "Do", + "load": "Załaduj listę", + "clearChecks": "Wyczyść zaznaczenia", + "empty": "Brak pozycji w tym zakresie dat.", + "checklistHint": "Zaznaczenia są zapisywane lokalnie w przeglądarce." + }, + "ingredientCatalog": { + "title": "Katalog składników", + "subtitle": "Wartości odżywcze na 100 g, pogrupowane według kategorii.", + "allCategories": "Wszystkie", + "searchPlaceholder": "Szukaj składników…", + "noResultsTitle": "Nie znaleziono składników", + "noResultsDescription": "Spróbuj innej kategorii lub frazy.", + "per100gNote": "Wszystkie wartości dotyczą 100 g.", + "addIngredient": "Dodaj składnik", + "editIngredient": "Edytuj składnik", + "selectCategory": "Wybierz kategorię", + "nameEn": "Nazwa (angielski)", + "namePlLabel": "Nazwa (polski)", + "formInvalid": "Wypełnij kategorię i nazwę.", + "deleteConfirm": "Usunąć ten składnik z katalogu?", + "columns": { + "name": "Składnik", + "category": "Kategoria", + "calories": "kcal", + "protein": "Białko (g)", + "fat": "Tłuszcz (g)", + "carbs": "Węglowodany (g)", + "fiber": "Błonnik (g)" + }, + "categories": { + "meat": "Mięso", + "poultry": "Drób", + "fish": "Ryby", + "seafood": "Owoce morza", + "vegetables": "Warzywa", + "fruits": "Owoce", + "dairy": "Nabiał", + "eggs": "Jaja", + "grains": "Zboża", + "legumes": "Rośliny strączkowe", + "nuts": "Orzechy i nasiona", + "oils": "Oleje i tłuszcze" + } + }, + "notFound": { + "title": "Nie znaleziono strony", + "description": "Strona, której szukasz, nie istnieje.", + "backToDashboard": "Wróć do panelu" + } +} diff --git a/meal-plan-frontend-angular/src/app/app.component.css b/meal-plan-frontend-angular/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/meal-plan-frontend-angular/src/app/app.component.ts b/meal-plan-frontend-angular/src/app/app.component.ts new file mode 100644 index 0000000..83d3238 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [RouterOutlet], + template: ``, +}) +export class AppComponent {} diff --git a/meal-plan-frontend-angular/src/app/app.config.ts b/meal-plan-frontend-angular/src/app/app.config.ts new file mode 100644 index 0000000..63e801b --- /dev/null +++ b/meal-plan-frontend-angular/src/app/app.config.ts @@ -0,0 +1,43 @@ +import { APP_INITIALIZER, ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { provideTranslateService } from '@ngx-translate/core'; +import { provideTranslateHttpLoader } from '@ngx-translate/http-loader'; + +import { routes } from './app.routes'; +import { AuthInterceptor } from './core/interceptors/auth.interceptor'; +import { AppearanceService } from './core/services/appearance.service'; +import { AuthService } from './core/services/auth.service'; +import { LanguageService } from './core/services/language.service'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideHttpClient(withInterceptorsFromDi()), + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, + ...provideTranslateService({ + fallbackLang: 'en', + loader: provideTranslateHttpLoader({ prefix: './i18n/', suffix: '.json' }), + }), + { + provide: APP_INITIALIZER, + useFactory: (language: LanguageService) => () => language.init(), + deps: [LanguageService], + multi: true, + }, + { + provide: APP_INITIALIZER, + useFactory: (auth: AuthService) => () => auth.restoreSession(), + deps: [AuthService], + multi: true, + }, + { + provide: APP_INITIALIZER, + useFactory: (appearance: AppearanceService) => () => appearance.init(), + deps: [AppearanceService], + multi: true, + }, + ], +}; diff --git a/meal-plan-frontend-angular/src/app/app.routes.ts b/meal-plan-frontend-angular/src/app/app.routes.ts new file mode 100644 index 0000000..5b16c78 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/app.routes.ts @@ -0,0 +1,48 @@ +import { Routes } from '@angular/router'; +import { authGuard, guestGuard } from './core/guards/auth.guard'; +import { AppLayoutComponent } from './layout/app-layout.component'; +import { LoginComponent } from './features/auth/login.component'; +import { RegisterComponent } from './features/auth/register.component'; +import { DashboardComponent } from './features/dashboard/dashboard.component'; +import { BreakfastComponent } from './features/category/breakfast.component'; +import { SecondBreakfastComponent } from './features/category/second-breakfast.component'; +import { LunchComponent } from './features/category/lunch.component'; +import { DinnerComponent } from './features/category/dinner.component'; +import { AllMealsComponent } from './features/all-meals/all-meals.component'; +import { ManageMealsComponent } from './features/manage-meals/manage-meals.component'; +import { IngredientCatalogComponent } from './features/ingredient-catalog/ingredient-catalog.component'; +import { DietTrackerComponent } from './features/diet-tracker/diet-tracker.component'; +import { MealPlannerComponent } from './features/meal-planner/meal-planner.component'; +import { ShoppingListComponent } from './features/shopping-list/shopping-list.component'; +import { RecipeDetailPageComponent } from './features/recipe-detail/recipe-detail-page.component'; +import { NotFoundComponent } from './features/not-found/not-found.component'; +import { DietGeneratorComponent } from './features/diet-generator/diet-generator.component'; +import { RecipeGeneratorComponent } from './features/recipe-generator/recipe-generator.component'; + +export const routes: Routes = [ + { path: 'login', component: LoginComponent, canActivate: [guestGuard] }, + { path: 'register', component: RegisterComponent, canActivate: [guestGuard] }, + { + path: '', + component: AppLayoutComponent, + canActivate: [authGuard], + children: [ + { path: '', component: DashboardComponent }, + { path: 'breakfast', component: BreakfastComponent }, + { path: 'second-breakfast', component: SecondBreakfastComponent }, + { path: 'lunch', component: LunchComponent }, + { path: 'dinner', component: DinnerComponent }, + { path: 'all-meals', component: AllMealsComponent }, + { path: 'manage-meals', component: ManageMealsComponent }, + { path: 'ingredients', component: IngredientCatalogComponent }, + { path: 'diet', component: DietTrackerComponent }, + { path: 'meal-planner', component: MealPlannerComponent }, + { path: 'diet-generator', component: DietGeneratorComponent }, + { path: 'recipe-generator', component: RecipeGeneratorComponent }, + { path: 'shopping-list', component: ShoppingListComponent }, + { path: 'recipes/:id', component: RecipeDetailPageComponent }, + { path: '404', component: NotFoundComponent }, + { path: '**', redirectTo: '404' }, + ], + }, +]; diff --git a/meal-plan-frontend-angular/src/app/core/guards/auth.guard.ts b/meal-plan-frontend-angular/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000..9a2e302 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/guards/auth.guard.ts @@ -0,0 +1,21 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +export const authGuard: CanActivateFn = () => { + const auth = inject(AuthService); + const router = inject(Router); + if (auth.isAuthenticated()) { + return true; + } + return router.createUrlTree(['/login']); +}; + +export const guestGuard: CanActivateFn = () => { + const auth = inject(AuthService); + const router = inject(Router); + if (!auth.isAuthenticated()) { + return true; + } + return router.createUrlTree(['/']); +}; diff --git a/meal-plan-frontend-angular/src/app/core/interceptors/auth.interceptor.ts b/meal-plan-frontend-angular/src/app/core/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..9b4ce52 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/interceptors/auth.interceptor.ts @@ -0,0 +1,118 @@ +import { Injectable, inject } from '@angular/core'; +import { + HttpBackend, + HttpClient, + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import { Router } from '@angular/router'; +import { Observable, catchError, firstValueFrom, from, switchMap, throwError } from 'rxjs'; +import { AuthService } from '../services/auth.service'; +import { TokenResponse } from '../models/auth.models'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + private isRefreshing = false; + private queue: Array<(token: string | null) => void> = []; + private readonly refreshClient: HttpClient; + + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + + constructor(httpBackend: HttpBackend) { + this.refreshClient = new HttpClient(httpBackend); + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const token = this.auth.getAccessToken(); + const authReq = token + ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) + : req; + + return next.handle(authReq).pipe( + catchError((error: HttpErrorResponse) => { + const original = authReq; + const isAuthEndpoint = + original.url.includes('/auth/login') || + original.url.includes('/auth/register') || + original.url.includes('/auth/refresh'); + + if (error.status !== 401 || isAuthEndpoint || original.headers.has('X-Retry')) { + return throwError(() => error); + } + + if (this.isRefreshing) { + return new Observable>((subscriber) => { + this.queue.push((newToken) => { + if (!newToken) { + subscriber.error(error); + return; + } + next + .handle( + original.clone({ + setHeaders: { + Authorization: `Bearer ${newToken}`, + 'X-Retry': 'true', + }, + }), + ) + .subscribe(subscriber); + }); + }); + } + + this.isRefreshing = true; + return from(this.performRefresh()).pipe( + switchMap((newToken) => { + this.isRefreshing = false; + this.flushQueue(newToken); + if (!newToken) { + this.redirectToLogin(); + return throwError(() => error); + } + return next.handle( + original.clone({ + setHeaders: { + Authorization: `Bearer ${newToken}`, + 'X-Retry': 'true', + }, + }), + ); + }), + ); + }), + ); + } + + private async performRefresh(): Promise { + const refreshToken = this.auth.getRefreshToken(); + if (!refreshToken) return null; + try { + const tokens = await firstValueFrom( + this.refreshClient.post('/api/auth/refresh', { refreshToken }), + ); + if (!tokens) return null; + this.auth.setTokens(tokens); + return tokens.accessToken; + } catch { + return null; + } + } + + private flushQueue(token: string | null): void { + this.queue.forEach((resolve) => resolve(token)); + this.queue = []; + } + + private redirectToLogin(): void { + this.auth.clearAuth(); + const path = this.router.url.split('?')[0]; + if (path !== '/login' && path !== '/register') { + void this.router.navigateByUrl('/login'); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/core/models/auth.models.ts b/meal-plan-frontend-angular/src/app/core/models/auth.models.ts new file mode 100644 index 0000000..40c1b06 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/auth.models.ts @@ -0,0 +1,23 @@ +export interface TokenResponse { + accessToken: string; + refreshToken: string; + expiresInSeconds: number; + tokenType: string; +} + +export interface UserInfo { + id: string; + userName: string | null; + email: string | null; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface RegisterRequest { + email: string; + password: string; + userName?: string; +} diff --git a/meal-plan-frontend-angular/src/app/core/models/diet.models.ts b/meal-plan-frontend-angular/src/app/core/models/diet.models.ts new file mode 100644 index 0000000..ee1eb02 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/diet.models.ts @@ -0,0 +1,237 @@ +export interface UserDailyGoals { + calorieGoal?: number | null; + proteinGoalG?: number | null; + fatGoalG?: number | null; + carbsGoalG?: number | null; + waterGoalMl?: number | null; +} + +export interface MacroRemaining { + goal?: number | null; + consumed: number; + remaining?: number | null; + progressPercent?: number | null; +} + +export interface DecimalMacroRemaining { + goal?: number | null; + consumed: number; + remaining?: number | null; + progressPercent?: number | null; +} + +export interface MealConsumption { + id: number; + recipeId?: number | null; + mealCategory: number; + logDate: string; + consumedAt: string; + name: string; + portions: number; + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + notes?: string | null; +} + +export interface DrinkCatalogItem { + id: number; + code: string; + name: string; + defaultVolumeMl: number; + caloriesPer100Ml?: number | null; + iconEmoji?: string | null; +} + +export interface DrinkConsumption { + id: number; + drinkCatalogId?: number | null; + logDate: string; + consumedAt: string; + name: string; + volumeMl: number; + calories?: number | null; +} + +export interface DailySummary { + date: string; + goals?: UserDailyGoals | null; + calories: MacroRemaining; + proteinG: DecimalMacroRemaining; + fatG: DecimalMacroRemaining; + carbsG: DecimalMacroRemaining; + waterMl: MacroRemaining; + meals: MealConsumption[]; + drinks: DrinkConsumption[]; +} + +export interface LogMealInput { + recipeId?: number; + catalogItemId?: number; + grams?: number; + name?: string; + mealCategory: number; + logDate?: string; + portions?: number; + calories?: number; + proteinG?: number; + fatG?: number; + carbsG?: number; + notes?: string; +} + +export interface MealPreview { + meal: { + name: string; + portions: number; + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + }; + remainingAfter: { + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + }; +} + +export interface DietSuggestion { + catalogItemId: number; + name: string; + namePl?: string | null; + categoryCode: string; + suggestedGrams: number; + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + reasonKey: string; +} + +export interface LogDrinkInput { + drinkCatalogId?: number; + name?: string; + volumeMl: number; + logDate?: string; + calories?: number; +} + +export interface DietReminder { + id: number; + reminderType: number; + title: string; + message?: string | null; + timeOfDay: string; + daysOfWeekMask: number; + mealCategory?: number | null; + isEnabled: boolean; +} + +export interface CreateDietReminderInput { + reminderType: number; + title: string; + message?: string; + timeOfDay: string; + daysOfWeekMask?: number; + mealCategory?: number; + isEnabled?: boolean; +} + +export type UpdateDietReminderInput = CreateDietReminderInput; + +export interface DueReminder { + id: number; + reminderType: number; + title: string; + message?: string | null; + timeOfDay: string; + mealCategory?: number | null; +} + +export interface HistoryDay { + date: string; + calories: number; + proteinG: number; + fatG: number; + carbsG: number; + waterMl: number; + mealCount: number; +} + +export interface RecentMeal { + recipeId?: number | null; + catalogItemId?: number | null; + name: string; + mealCategory: number; + portions: number; + grams?: number | null; + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + lastLoggedAt: string; +} + +export interface FavoriteRecipe { + recipeId: number; + name: string; + mealCategory: number; + calories: number | null; +} + +export interface FavoriteCatalogItem { + catalogItemId: number; + name: string; + namePl?: string | null; + categoryCode: string; +} + +export interface CopyMealsResult { + sourceDate: string; + targetDate: string; + copiedCount: number; +} + +export enum DietReminderType { + Water = 0, + Meal = 1, + Snack = 2, + Custom = 3, +} + +export const CONSUMPTION_CATEGORIES = [ + { value: 0, labelKey: 'categories.breakfast' }, + { value: 1, labelKey: 'categories.secondBreakfast' }, + { value: 2, labelKey: 'categories.lunch' }, + { value: 3, labelKey: 'categories.dinner' }, + { value: 4, labelKey: 'diet.snack' }, +] as const; + +export const REMINDER_DAYS_MASK_ALL = 127; + +function todayIso(): string { + return new Date().toISOString().slice(0, 10); +} + +function addDaysIso(iso: string, days: number): string { + const d = new Date(`${iso}T12:00:00`); + d.setDate(d.getDate() + days); + return d.toISOString().slice(0, 10); +} + +function startOfWeekIso(iso: string): string { + const d = new Date(`${iso}T12:00:00`); + const day = d.getDay(); + const diff = (day === 0 ? -6 : 1) - day; + d.setDate(d.getDate() + diff); + return d.toISOString().slice(0, 10); +} + +function endOfWeekIso(iso: string): string { + return addDaysIso(startOfWeekIso(iso), 6); +} + +export { todayIso, addDaysIso, startOfWeekIso, endOfWeekIso }; diff --git a/meal-plan-frontend-angular/src/app/core/models/generator.models.ts b/meal-plan-frontend-angular/src/app/core/models/generator.models.ts new file mode 100644 index 0000000..23eaa3e --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/generator.models.ts @@ -0,0 +1,149 @@ +export interface DietUserProfile { + heightCm: number; + weightKg: number; + age: number; + sex: 'male' | 'female'; + activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'; + goal: 'lose_weight' | 'maintain' | 'gain_muscle' | 'recomp'; + allergiesNotes?: string | null; + dislikedFoods?: string | null; + mealsPerDayMask: number; +} + +export interface MacroProposal { + calories: number; + proteinG: number; + fatG: number; + carbsG: number; + rationale?: string | null; +} + +export interface DietGeneratorDraftMealIngredient { + id: number; + name: string; + amountGrams?: number | null; + unit?: string | null; + sortOrder: number; + sourceIngredientId?: number | null; +} + +export interface DietGeneratorDraftMealStep { + id: number; + stepNumber: number; + description: string; +} + +export interface DietGeneratorDraftMeal { + id: number; + planDate: string; + mealCategory: number; + recipeId: number; + recipeName: string; + portions: number; + calories?: number | null; + proteinG?: number | null; + fatG?: number | null; + carbsG?: number | null; + isLocked: boolean; + prepTimeMinutes?: number | null; + ingredients: DietGeneratorDraftMealIngredient[]; + steps: DietGeneratorDraftMealStep[]; +} + +export interface DietGeneratorDaySummary { + planDate: string; + totalCalories: number; + targetCalories: number; + withinTolerance: boolean; +} + +export interface DietGeneratorSession { + id: number; + status: string; + proposedMacros?: MacroProposal | null; + planStartDate?: string | null; + planDayCount: number; + calorieToleranceKcal: number; + draftMeals: DietGeneratorDraftMeal[]; + daySummaries: DietGeneratorDaySummary[]; +} + +export interface CommitDietPlanResult { + sessionId: number; + planStartDate: string; + planEndDate: string; + mealsWritten: number; +} + +export interface RecipeGeneratorConstraints { + prompt: string; + mealCategory: number; + targetCalories?: number | null; + calorieToleranceKcal?: number; + maxPrepTimeMinutes?: number | null; + cuisineStyle?: string | null; + dietStyle?: string | null; + exclusions?: string | null; +} + +export interface GeneratedIngredient { + name: string; + amountGrams?: number | null; + unit?: string | null; + catalogItemId?: number | null; + catalogName?: string | null; + isLinked: boolean; +} + +export interface GeneratedStep { + stepNumber: number; + description: string; +} + +export interface GeneratedRecipeDraft { + draftId: string; + name: string; + mealCategory: number; + prepTimeMinutes?: number | null; + calories?: number | null; + protein?: number | null; + fat?: number | null; + carbs?: number | null; + ingredients: GeneratedIngredient[]; + steps: GeneratedStep[]; + unlinkedIngredientCount: number; +} + +export interface RecipeGeneratorSession { + id: number; + status: string; + constraints: RecipeGeneratorConstraints; + draft?: GeneratedRecipeDraft | null; + drafts: GeneratedRecipeDraft[]; + savedRecipeId?: number | null; + generationVersion: number; +} + +export interface CommitRecipeResult { + sessionId: number; + recipeId: number; + recipeName: string; + draftId: string; +} + +export interface CommitRecipesResult { + sessionId: number; + saved: CommitRecipeResult[]; +} + +export const DEFAULT_DIET_PROFILE: DietUserProfile = { + heightCm: 175, + weightKg: 75, + age: 30, + sex: 'male', + activityLevel: 'moderate', + goal: 'maintain', + mealsPerDayMask: 15, + allergiesNotes: '', + dislikedFoods: '', +}; diff --git a/meal-plan-frontend-angular/src/app/core/models/ingredient-catalog.models.ts b/meal-plan-frontend-angular/src/app/core/models/ingredient-catalog.models.ts new file mode 100644 index 0000000..f265cdd --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/ingredient-catalog.models.ts @@ -0,0 +1,33 @@ +export interface IngredientCategory { + id: number; + code: string; + name: string; + itemCount: number; +} + +export interface IngredientNutritionItem { + id: number; + categoryId: number; + categoryCode: string; + categoryName: string; + name: string; + namePl?: string | null; + caloriesPer100G: number; + proteinGPer100G: number; + fatGPer100G: number; + carbsGPer100G: number; + fiberGPer100G: number | null; +} + +export interface UpsertIngredientNutritionItemInput { + categoryId: number; + name: string; + namePl?: string | null; + caloriesPer100G: number; + proteinGPer100G: number; + fatGPer100G: number; + carbsGPer100G: number; + fiberGPer100G?: number | null; + sortOrder?: number; + isActive?: boolean; +} diff --git a/meal-plan-frontend-angular/src/app/core/models/meal-plan.models.ts b/meal-plan-frontend-angular/src/app/core/models/meal-plan.models.ts new file mode 100644 index 0000000..482f6e8 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/meal-plan.models.ts @@ -0,0 +1,24 @@ +export interface MealPlanEntry { + id: number; + planDate: string; + mealCategory: number; + recipeId: number; + recipeName: string; + portions: number; + calories: number | null; +} + +export interface UpsertMealPlanEntryInput { + planDate: string; + mealCategory: number; + recipeId: number; + portions?: number; +} + +export interface ShoppingListItem { + name: string; + unit: string | null; + totalGrams: number | null; + totalAmount: number | null; + breakdown: string[]; +} diff --git a/meal-plan-frontend-angular/src/app/core/models/recipe.models.ts b/meal-plan-frontend-angular/src/app/core/models/recipe.models.ts new file mode 100644 index 0000000..b482f36 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/models/recipe.models.ts @@ -0,0 +1,100 @@ +export enum MealCategory { + Breakfast = 0, + SecondBreakfast = 1, + Lunch = 2, + Dinner = 3, +} + +export const MEAL_CATEGORY_ROUTES: Record = { + [MealCategory.Breakfast]: '/breakfast', + [MealCategory.SecondBreakfast]: '/second-breakfast', + [MealCategory.Lunch]: '/lunch', + [MealCategory.Dinner]: '/dinner', +}; + +export const MEAL_CATEGORY_I18N_KEYS: Record = { + [MealCategory.Breakfast]: 'categories.breakfast', + [MealCategory.SecondBreakfast]: 'categories.secondBreakfast', + [MealCategory.Lunch]: 'categories.lunch', + [MealCategory.Dinner]: 'categories.dinner', +}; + +export interface RecipeListItem { + id: number; + name: string; + mealCategory: MealCategory; + calories: number | null; + prepTimeMinutes: number | null; +} + +export interface Ingredient { + id: number; + name: string; + amountGrams: number | null; + unit: string | null; + sortOrder: number; + catalogItemId?: number | null; +} + +export interface RecipeStep { + id: number; + stepNumber: number; + description: string; +} + +export interface RecipeDetail { + id: number; + name: string; + mealCategory: MealCategory; + calories: number | null; + protein: number | null; + fat: number | null; + carbs: number | null; + prepTimeMinutes: number | null; + ingredients: Ingredient[]; + steps: RecipeStep[]; +} + +export interface CategoryCount { + category: MealCategory; + name: string; + count: number; +} + +export interface CreateIngredientInput { + name: string; + amountGrams: number | null; + unit: string; + sortOrder: number; + catalogItemId?: number | null; +} + +export interface CreateStepInput { + stepNumber: number; + description: string; +} + +export interface CreateRecipeInput { + name: string; + mealCategory: MealCategory; + calories: number | null; + protein: number | null; + fat: number | null; + carbs: number | null; + prepTimeMinutes: number | null; + ingredients: CreateIngredientInput[]; + steps: CreateStepInput[]; +} + +export interface RecipeImportResult { + createdCount: number; + skippedCount: number; + errors: string[]; + createdRecipeIds: number[]; +} + +export interface RecipeMacroRecalcResult { + recipesProcessed: number; + recipesUpdated: number; + unlinkedIngredientRows: number; +} diff --git a/meal-plan-frontend-angular/src/app/core/services/appearance.service.ts b/meal-plan-frontend-angular/src/app/core/services/appearance.service.ts new file mode 100644 index 0000000..08b8ff1 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/appearance.service.ts @@ -0,0 +1,54 @@ +import { Injectable, signal } from '@angular/core'; +import { + AppearanceId, + IconStyleId, + THEME_PROPOSALS, + ThemeProposal, +} from '../theme/appearance.config'; + +const STORAGE_KEY = 'dailymeals.appearance'; + +@Injectable({ providedIn: 'root' }) +export class AppearanceService { + readonly proposal = signal(this.load()); + + readonly iconStyle = signal(this.proposal().iconStyle); + + init(): void { + this.apply(this.proposal()); + } + + setProposal(id: AppearanceId): void { + const next = THEME_PROPOSALS.find((p) => p.id === id) ?? THEME_PROPOSALS[0]; + this.proposal.set(next); + this.iconStyle.set(next.iconStyle); + localStorage.setItem(STORAGE_KEY, id); + this.apply(next); + } + + proposals(): ThemeProposal[] { + return THEME_PROPOSALS; + } + + strokeWidth(): string { + return this.iconStyle() === 'bold' ? '2.5' : '2'; + } + + useEmojiIcons(): boolean { + return this.iconStyle() === 'emoji'; + } + + private load(): ThemeProposal { + const stored = localStorage.getItem(STORAGE_KEY) as AppearanceId | null; + const found = THEME_PROPOSALS.find((p) => p.id === stored); + const proposal = found ?? THEME_PROPOSALS[0]; + this.apply(proposal); + return proposal; + } + + private apply(proposal: ThemeProposal): void { + const root = document.documentElement; + root.dataset['colorTheme'] = proposal.colorTheme; + root.dataset['iconStyle'] = proposal.iconStyle; + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/auth.service.ts b/meal-plan-frontend-angular/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..958c5d6 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/auth.service.ts @@ -0,0 +1,111 @@ +import { Injectable, signal } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; +import { + LoginRequest, + RegisterRequest, + TokenResponse, + UserInfo, +} from '../models/auth.models'; + +const REFRESH_TOKEN_KEY = 'dm_refresh_token'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly accessToken = signal(null); + private readonly refreshToken = signal(null); + readonly user = signal(null); + + constructor(private readonly http: HttpClient) { + const stored = localStorage.getItem(REFRESH_TOKEN_KEY); + if (stored) { + this.refreshToken.set(stored); + } + } + + isAuthenticated(): boolean { + return Boolean(this.accessToken()); + } + + getAccessToken(): string | null { + return this.accessToken(); + } + + getRefreshToken(): string | null { + return this.refreshToken(); + } + + setTokens(tokens: TokenResponse): void { + this.accessToken.set(tokens.accessToken); + this.refreshToken.set(tokens.refreshToken); + localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken); + } + + clearAuth(): void { + this.accessToken.set(null); + this.refreshToken.set(null); + this.user.set(null); + localStorage.removeItem(REFRESH_TOKEN_KEY); + } + + async login(payload: LoginRequest): Promise { + const tokens = await firstValueFrom(this.http.post('/api/auth/login', payload)); + this.setTokens(tokens); + return this.loadCurrentUser(); + } + + async register(payload: RegisterRequest): Promise { + const tokens = await firstValueFrom(this.http.post('/api/auth/register', payload)); + this.setTokens(tokens); + return this.loadCurrentUser(); + } + + async loadCurrentUser(): Promise { + const me = await firstValueFrom(this.http.get('/api/auth/me')); + this.user.set(me); + return me; + } + + async restoreSession(): Promise { + const refresh = this.refreshToken(); + if (!refresh) return false; + const token = await this.refreshAccessToken(); + if (!token) { + this.clearAuth(); + return false; + } + try { + await this.loadCurrentUser(); + return true; + } catch { + this.clearAuth(); + return false; + } + } + + async logout(): Promise { + const refresh = this.refreshToken(); + try { + if (refresh) { + await firstValueFrom(this.http.post('/api/auth/logout', { refreshToken: refresh })); + } + } catch { + // best effort + } + this.clearAuth(); + } + + async refreshAccessToken(): Promise { + const refresh = this.refreshToken(); + if (!refresh) return null; + try { + const tokens = await firstValueFrom( + this.http.post('/api/auth/refresh', { refreshToken: refresh }), + ); + this.setTokens(tokens); + return tokens.accessToken; + } catch { + return null; + } + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/diet-generator.service.ts b/meal-plan-frontend-angular/src/app/core/services/diet-generator.service.ts new file mode 100644 index 0000000..a8e745b --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/diet-generator.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; +import { + CommitDietPlanResult, + DietGeneratorSession, + DietUserProfile, + MacroProposal, +} from '../models/generator.models'; + +@Injectable({ providedIn: 'root' }) +export class DietGeneratorService { + constructor(private readonly http: HttpClient) {} + + fetchProfile(): Observable { + return this.http.get('/api/diet-generator/profile').pipe( + map((data) => (data.heightCm ? data : null)), + ); + } + + saveProfile(profile: DietUserProfile): Observable { + return this.http.put('/api/diet-generator/profile', profile); + } + + createSession(input: { + planStartDate?: string; + planDayCount?: number; + calorieToleranceKcal?: number; + }): Observable { + return this.http.post('/api/diet-generator/sessions', input); + } + + fetchSession(id: number): Observable { + return this.http.get(`/api/diet-generator/sessions/${id}`); + } + + proposeMacros(sessionId: number): Observable { + return this.http.post( + `/api/diet-generator/sessions/${sessionId}/propose-macros`, + null, + ); + } + + acceptMacros(sessionId: number, macros?: Partial): Observable { + return this.http.post( + `/api/diet-generator/sessions/${sessionId}/accept-macros`, + { + calories: macros?.calories, + proteinG: macros?.proteinG, + fatG: macros?.fatG, + carbsG: macros?.carbsG, + }, + ); + } + + generatePlan(sessionId: number): Observable { + return this.http.post( + `/api/diet-generator/sessions/${sessionId}/generate-plan`, + null, + ); + } + + regenerateMeal( + sessionId: number, + planDate: string, + mealCategory: number, + ): Observable { + return this.http.post( + `/api/diet-generator/sessions/${sessionId}/regenerate-meal`, + { planDate, mealCategory }, + ); + } + + toggleMealLock(sessionId: number, mealId: number, isLocked: boolean): Observable { + return this.http.patch( + `/api/diet-generator/sessions/${sessionId}/meals/${mealId}`, + { isLocked }, + ); + } + + commitPlan(sessionId: number): Observable { + return this.http.post( + `/api/diet-generator/sessions/${sessionId}/commit`, + null, + ); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/diet.service.ts b/meal-plan-frontend-angular/src/app/core/services/diet.service.ts new file mode 100644 index 0000000..83f3d74 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/diet.service.ts @@ -0,0 +1,129 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, map } from 'rxjs'; +import { + CopyMealsResult, + CreateDietReminderInput, + DailySummary, + DietReminder, + DietSuggestion, + DrinkCatalogItem, + DueReminder, + FavoriteCatalogItem, + FavoriteRecipe, + HistoryDay, + LogDrinkInput, + LogMealInput, + MealConsumption, + MealPreview, + RecentMeal, + UpdateDietReminderInput, + UserDailyGoals, + todayIso, +} from '../models/diet.models'; +import { normalizeDailySummary } from '../utils/diet-normalize.util'; + +@Injectable({ providedIn: 'root' }) +export class DietService { + constructor(private readonly http: HttpClient) {} + + getSummary(date = todayIso()): Observable { + return this.http + .get('/api/diet/summary', { params: { date } }) + .pipe(map(normalizeDailySummary)); + } + + getGoals(): Observable { + return this.http.get('/api/diet/goals'); + } + + saveGoals(goals: UserDailyGoals): Observable { + return this.http.put('/api/diet/goals', goals); + } + + logMeal(input: LogMealInput): Observable { + return this.http.post('/api/diet/meals', input); + } + + previewMeal(input: LogMealInput, date = todayIso()): Observable { + return this.http.post('/api/diet/meals/preview', input, { params: { date } }); + } + + getSuggestions(date = todayIso(), limit = 5): Observable { + return this.http.get('/api/diet/suggestions', { params: { date, limit } }); + } + + deleteMeal(id: number): Observable { + return this.http.delete(`/api/diet/meals/${id}`); + } + + getDrinkCatalog(): Observable { + return this.http.get('/api/diet/drinks/catalog'); + } + + logDrink(input: LogDrinkInput): Observable { + return this.http.post('/api/diet/drinks', input); + } + + deleteDrink(id: number): Observable { + return this.http.delete(`/api/diet/drinks/${id}`); + } + + getReminders(): Observable { + return this.http.get('/api/diet/reminders'); + } + + getDueReminders(date = todayIso(), time?: string): Observable { + const params: Record = { date }; + if (time) params['time'] = time; + return this.http.get('/api/diet/reminders/due', { params }); + } + + createReminder(input: CreateDietReminderInput): Observable { + return this.http.post('/api/diet/reminders', input); + } + + updateReminder(id: number, input: UpdateDietReminderInput): Observable { + return this.http.put(`/api/diet/reminders/${id}`, input); + } + + deleteReminder(id: number): Observable { + return this.http.delete(`/api/diet/reminders/${id}`); + } + + getHistory(from: string, to: string): Observable { + return this.http.get('/api/diet/history', { params: { from, to } }); + } + + getRecentMeals(limit = 10): Observable { + return this.http.get('/api/diet/meals/recent', { params: { limit } }); + } + + copyYesterdayMeals(date = todayIso()): Observable { + return this.http.post('/api/diet/meals/copy-yesterday', null, { params: { date } }); + } + + getFavoriteRecipes(): Observable { + return this.http.get('/api/diet/favorites/recipes'); + } + + addFavoriteRecipe(recipeId: number): Observable { + return this.http.post(`/api/diet/favorites/recipes/${recipeId}`, null); + } + + removeFavoriteRecipe(recipeId: number): Observable { + return this.http.delete(`/api/diet/favorites/recipes/${recipeId}`); + } + + getFavoriteCatalogItems(): Observable { + return this.http.get('/api/diet/favorites/catalog'); + } + + addFavoriteCatalogItem(catalogItemId: number): Observable { + return this.http.post(`/api/diet/favorites/catalog/${catalogItemId}`, null); + } + + removeFavoriteCatalogItem(catalogItemId: number): Observable { + return this.http.delete(`/api/diet/favorites/catalog/${catalogItemId}`); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/ingredient-catalog.service.ts b/meal-plan-frontend-angular/src/app/core/services/ingredient-catalog.service.ts new file mode 100644 index 0000000..cc12237 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/ingredient-catalog.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { + IngredientCategory, + IngredientNutritionItem, + UpsertIngredientNutritionItemInput, +} from '../models/ingredient-catalog.models'; + +@Injectable({ providedIn: 'root' }) +export class IngredientCatalogService { + constructor(private readonly http: HttpClient) {} + + getCategories(): Observable { + return this.http.get('/api/ingredient-catalog/categories'); + } + + getItems(categoryCode?: string, search?: string): Observable { + let params = new HttpParams(); + if (categoryCode) params = params.set('category', categoryCode); + if (search) params = params.set('search', search); + return this.http.get('/api/ingredient-catalog', { params }); + } + + getItem(id: number): Observable { + return this.http.get(`/api/ingredient-catalog/${id}`); + } + + createItem(input: UpsertIngredientNutritionItemInput): Observable { + return this.http.post('/api/ingredient-catalog', input); + } + + updateItem(id: number, input: UpsertIngredientNutritionItemInput): Observable { + return this.http.put(`/api/ingredient-catalog/${id}`, input); + } + + deleteItem(id: number): Observable { + return this.http.delete(`/api/ingredient-catalog/${id}`); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/language.service.ts b/meal-plan-frontend-angular/src/app/core/services/language.service.ts new file mode 100644 index 0000000..460a5d7 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/language.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +const STORAGE_KEY = 'dailymeals.lang'; + +@Injectable({ providedIn: 'root' }) +export class LanguageService { + constructor(private readonly translate: TranslateService) {} + + init(): void { + const stored = localStorage.getItem(STORAGE_KEY); + const lang = + stored === 'en' || stored === 'pl' + ? stored + : navigator.language.toLowerCase().startsWith('pl') + ? 'pl' + : 'en'; + this.translate.addLangs(['en', 'pl']); + this.translate.setFallbackLang('en'); + this.setLanguage(lang); + } + + setLanguage(lang: 'en' | 'pl'): void { + this.translate.use(lang); + localStorage.setItem(STORAGE_KEY, lang); + document.documentElement.lang = lang; + } + + currentLanguage(): 'en' | 'pl' { + const current = this.translate.getCurrentLang(); + return current?.startsWith('pl') ? 'pl' : 'en'; + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/layout.service.ts b/meal-plan-frontend-angular/src/app/core/services/layout.service.ts new file mode 100644 index 0000000..f7fb51b --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/layout.service.ts @@ -0,0 +1,24 @@ +import { Injectable, signal } from '@angular/core'; + +export type LayoutId = 'sidebar' | 'topnav' | 'compact' | 'wide'; + +const STORAGE_KEY = 'dailymeals.layout'; +const VALID: LayoutId[] = ['sidebar', 'topnav', 'compact', 'wide']; + +@Injectable({ providedIn: 'root' }) +export class LayoutService { + readonly layout = signal(this.load()); + + setLayout(id: LayoutId): void { + this.layout.set(id); + localStorage.setItem(STORAGE_KEY, id); + document.documentElement.dataset['layout'] = id; + } + + private load(): LayoutId { + const stored = localStorage.getItem(STORAGE_KEY); + const id = VALID.includes(stored as LayoutId) ? (stored as LayoutId) : 'sidebar'; + document.documentElement.dataset['layout'] = id; + return id; + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/meal-plan.service.ts b/meal-plan-frontend-angular/src/app/core/services/meal-plan.service.ts new file mode 100644 index 0000000..3231c79 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/meal-plan.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { + MealPlanEntry, + ShoppingListItem, + UpsertMealPlanEntryInput, +} from '../models/meal-plan.models'; + +@Injectable({ providedIn: 'root' }) +export class MealPlanService { + constructor(private readonly http: HttpClient) {} + + getEntries(from: string, to: string): Observable { + return this.http.get('/api/meal-plan', { params: { from, to } }); + } + + upsertEntry(input: UpsertMealPlanEntryInput): Observable { + return this.http.put('/api/meal-plan', input); + } + + deleteEntry(id: number): Observable { + return this.http.delete(`/api/meal-plan/${id}`); + } + + getShoppingList(from: string, to: string): Observable { + return this.http.get('/api/meal-plan/shopping-list', { params: { from, to } }); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/recipe-generator.service.ts b/meal-plan-frontend-angular/src/app/core/services/recipe-generator.service.ts new file mode 100644 index 0000000..b93d579 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/recipe-generator.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { + CommitRecipesResult, + GeneratedRecipeDraft, + RecipeGeneratorConstraints, + RecipeGeneratorSession, +} from '../models/generator.models'; + +@Injectable({ providedIn: 'root' }) +export class RecipeGeneratorService { + constructor(private readonly http: HttpClient) {} + + createSession(constraints: RecipeGeneratorConstraints): Observable { + return this.http.post('/api/recipe-generator/sessions', constraints); + } + + fetchSession(id: number): Observable { + return this.http.get(`/api/recipe-generator/sessions/${id}`); + } + + generateDrafts(sessionId: number, count = 3): Observable { + return this.http.post(`/api/recipe-generator/sessions/${sessionId}/generate`, { + count, + }); + } + + regeneratePart( + sessionId: number, + draftId: string, + mode: 'ingredients' | 'steps' | 'full', + instruction?: string, + ): Observable { + return this.http.post(`/api/recipe-generator/sessions/${sessionId}/regenerate`, { + draftId, + mode, + instruction, + }); + } + + removeDraft(sessionId: number, draftId: string): Observable { + return this.http.delete( + `/api/recipe-generator/sessions/${sessionId}/drafts/${encodeURIComponent(draftId)}`, + ); + } + + updateDraft(sessionId: number, draft: GeneratedRecipeDraft): Observable { + return this.http.put(`/api/recipe-generator/sessions/${sessionId}/draft`, draft); + } + + commitRecipes(sessionId: number, draftIds?: string[]): Observable { + return this.http.post(`/api/recipe-generator/sessions/${sessionId}/commit`, { + draftIds: draftIds?.length ? draftIds : undefined, + }); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/recipe-management.service.ts b/meal-plan-frontend-angular/src/app/core/services/recipe-management.service.ts new file mode 100644 index 0000000..3afc71e --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/recipe-management.service.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { + CreateRecipeInput, + RecipeDetail, + RecipeImportResult, + RecipeMacroRecalcResult, +} from '../models/recipe.models'; + +@Injectable({ providedIn: 'root' }) +export class RecipeManagementService { + constructor(private readonly http: HttpClient) {} + + createRecipe(input: CreateRecipeInput): Observable { + return this.http.post('/api/recipes', this.toPayload(input)); + } + + updateRecipe(id: number, input: CreateRecipeInput): Observable { + return this.http.put(`/api/recipes/${id}`, this.toPayload(input)); + } + + downloadImportTemplate(): Observable { + return this.http.get('/api/recipes/import/template', { responseType: 'blob' }); + } + + importFromExcel(file: File): Observable { + const formData = new FormData(); + formData.append('file', file); + return this.http.post('/api/recipes/import/excel', formData); + } + + recalculateAllMacros(): Observable { + return this.http.post('/api/recipes/recalculate-macros', {}); + } + + private toPayload(input: CreateRecipeInput) { + return { + name: input.name, + mealCategory: input.mealCategory, + calories: input.calories, + protein: input.protein, + fat: input.fat, + carbs: input.carbs, + prepTimeMinutes: input.prepTimeMinutes, + ingredients: input.ingredients.map((i, index) => ({ + name: i.name, + amountGrams: i.amountGrams, + unit: i.unit.trim() || null, + sortOrder: i.sortOrder || index, + catalogItemId: i.catalogItemId ?? null, + })), + steps: input.steps.map((s) => ({ + stepNumber: s.stepNumber, + description: s.description, + })), + }; + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/recipe.service.ts b/meal-plan-frontend-angular/src/app/core/services/recipe.service.ts new file mode 100644 index 0000000..4770b3e --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/recipe.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { + CategoryCount, + MealCategory, + RecipeDetail, + RecipeListItem, +} from '../models/recipe.models'; + +export interface RecipeQuery { + category?: MealCategory; + search?: string; + ingredient?: string; +} + +@Injectable({ providedIn: 'root' }) +export class RecipeService { + constructor(private readonly http: HttpClient) {} + + getRecipes(query: RecipeQuery = {}): Observable { + let params = new HttpParams(); + if (query.category != null) params = params.set('category', query.category); + if (query.search) params = params.set('search', query.search); + if (query.ingredient) params = params.set('ingredient', query.ingredient); + return this.http.get('/api/recipes', { params }); + } + + getRecipeById(id: number): Observable { + return this.http.get(`/api/recipes/${id}`); + } + + getCategories(): Observable { + return this.http.get('/api/recipes/categories'); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/reminder-notification.service.ts b/meal-plan-frontend-angular/src/app/core/services/reminder-notification.service.ts new file mode 100644 index 0000000..3d431f7 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/reminder-notification.service.ts @@ -0,0 +1,61 @@ +import { Injectable, OnDestroy, inject } from '@angular/core'; +import { interval, Subscription } from 'rxjs'; +import { DietService } from './diet.service'; +import { AuthService } from './auth.service'; +import { DueReminder } from '../models/diet.models'; +import { todayIso } from '../models/diet.models'; + +@Injectable({ providedIn: 'root' }) +export class ReminderNotificationService implements OnDestroy { + private readonly diet = inject(DietService); + private readonly auth = inject(AuthService); + private sub: Subscription | null = null; + private readonly notifiedIds = new Set(); + + start(): void { + if (this.sub || !this.auth.isAuthenticated()) return; + this.requestPermission(); + this.poll(); + this.sub = interval(60_000).subscribe(() => this.poll()); + } + + stop(): void { + this.sub?.unsubscribe(); + this.sub = null; + this.notifiedIds.clear(); + } + + ngOnDestroy(): void { + this.stop(); + } + + private requestPermission(): void { + if (typeof Notification === 'undefined') return; + if (Notification.permission === 'default') { + void Notification.requestPermission(); + } + } + + private poll(): void { + if (!this.auth.isAuthenticated()) return; + const now = new Date(); + const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; + this.diet.getDueReminders(todayIso(), time).subscribe({ + next: (due) => this.showNotifications(due), + error: () => {}, + }); + } + + private showNotifications(due: DueReminder[]): void { + if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return; + for (const r of due) { + if (this.notifiedIds.has(r.id)) continue; + this.notifiedIds.add(r.id); + const body = r.message ?? undefined; + new Notification(r.title, { body, tag: `reminder-${r.id}` }); + } + if (this.notifiedIds.size > 100) { + this.notifiedIds.clear(); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/core/services/theme.service.ts b/meal-plan-frontend-angular/src/app/core/services/theme.service.ts new file mode 100644 index 0000000..5fea587 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/services/theme.service.ts @@ -0,0 +1,27 @@ +import { Injectable, signal } from '@angular/core'; + +export type Theme = 'light' | 'dark'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + readonly theme = signal(this.detectInitial()); + + constructor() { + this.apply(this.theme()); + } + + toggle(): void { + const next = this.theme() === 'dark' ? 'light' : 'dark'; + this.theme.set(next); + this.apply(next); + } + + private detectInitial(): Theme { + if (typeof window === 'undefined') return 'light'; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + private apply(theme: Theme): void { + document.documentElement.classList.toggle('dark', theme === 'dark'); + } +} diff --git a/meal-plan-frontend-angular/src/app/core/theme/appearance.config.ts b/meal-plan-frontend-angular/src/app/core/theme/appearance.config.ts new file mode 100644 index 0000000..9c0e752 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/theme/appearance.config.ts @@ -0,0 +1,62 @@ +export type ColorThemeId = 'forest' | 'ocean' | 'sunset' | 'berry'; +export type IconStyleId = 'outline' | 'emoji' | 'bold'; +export type AppearanceId = ColorThemeId; + +export interface ThemeProposal { + id: AppearanceId; + labelKey: string; + descriptionKey: string; + colorTheme: ColorThemeId; + iconStyle: IconStyleId; + /** Preview swatch for UI */ + swatch: string; +} + +export const THEME_PROPOSALS: ThemeProposal[] = [ + { + id: 'forest', + labelKey: 'appearance.forest', + descriptionKey: 'appearance.forestDesc', + colorTheme: 'forest', + iconStyle: 'outline', + swatch: '#16774c', + }, + { + id: 'ocean', + labelKey: 'appearance.ocean', + descriptionKey: 'appearance.oceanDesc', + colorTheme: 'ocean', + iconStyle: 'outline', + swatch: '#2563eb', + }, + { + id: 'sunset', + labelKey: 'appearance.sunset', + descriptionKey: 'appearance.sunsetDesc', + colorTheme: 'sunset', + iconStyle: 'emoji', + swatch: '#d97706', + }, + { + id: 'berry', + labelKey: 'appearance.berry', + descriptionKey: 'appearance.berryDesc', + colorTheme: 'berry', + iconStyle: 'bold', + swatch: '#7c3aed', + }, +]; + +export const NAV_ICON_EMOJI: Record = { + dashboard: '📊', + coffee: '☕', + croissant: '🥐', + soup: '🍲', + moon: '🌙', + list: '📋', + book: '✏️', + leaf: '🥬', + diet: '🥗', + calendar: '📅', + cart: '🛒', +}; diff --git a/meal-plan-frontend-angular/src/app/core/utils/api-error.util.ts b/meal-plan-frontend-angular/src/app/core/utils/api-error.util.ts new file mode 100644 index 0000000..f3257b8 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/api-error.util.ts @@ -0,0 +1,19 @@ +import { HttpErrorResponse } from '@angular/common/http'; + +const GENERIC_API_TITLES = new Set([ + 'An unexpected error occurred.', + 'Internal Server Error', +]); + +export function extractApiError(error: unknown, fallback = 'Something went wrong.'): string { + if (error instanceof HttpErrorResponse) { + const data = error.error as { error?: string; title?: string } | undefined; + if (data?.error) return data.error; + if (data?.title && !GENERIC_API_TITLES.has(data.title)) return data.title; + return fallback; + } + if (error instanceof Error) { + return error.message; + } + return fallback; +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/diet-normalize.util.ts b/meal-plan-frontend-angular/src/app/core/utils/diet-normalize.util.ts new file mode 100644 index 0000000..d4dc8f2 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/diet-normalize.util.ts @@ -0,0 +1,83 @@ +import { MacroRemaining, DecimalMacroRemaining, DailySummary } from '../models/diet.models'; + +const EMPTY_INT_MACRO: MacroRemaining = { + consumed: 0, + goal: null, + remaining: null, + progressPercent: null, +}; + +const EMPTY_DECIMAL_MACRO: DecimalMacroRemaining = { + consumed: 0, + goal: null, + remaining: null, + progressPercent: null, +}; + +function pick(raw: Record, camel: string, pascal: string): T | undefined { + return (raw[camel] ?? raw[pascal]) as T | undefined; +} + +function normalizeIntMacro(raw: unknown): MacroRemaining { + if (!raw || typeof raw !== 'object') return { ...EMPTY_INT_MACRO }; + const obj = raw as Record; + return { + goal: (pick(obj, 'goal', 'Goal') ?? null) as number | null, + consumed: Number(pick(obj, 'consumed', 'Consumed') ?? 0), + remaining: (pick(obj, 'remaining', 'Remaining') ?? null) as number | null, + progressPercent: (pick(obj, 'progressPercent', 'ProgressPercent') ?? null) as + | number + | null, + }; +} + +function normalizeDecimalMacro(raw: unknown): DecimalMacroRemaining { + if (!raw || typeof raw !== 'object') return { ...EMPTY_DECIMAL_MACRO }; + const obj = raw as Record; + return { + goal: (pick(obj, 'goal', 'Goal') ?? null) as number | null, + consumed: Number(pick(obj, 'consumed', 'Consumed') ?? 0), + remaining: (pick(obj, 'remaining', 'Remaining') ?? null) as number | null, + progressPercent: (pick(obj, 'progressPercent', 'ProgressPercent') ?? null) as + | number + | null, + }; +} + +/** Normalizes API payloads so templates never crash on missing nested fields. */ +export function normalizeDailySummary(raw: unknown): DailySummary { + if (!raw || typeof raw !== 'object') { + return { + date: '', + goals: null, + calories: { ...EMPTY_INT_MACRO }, + proteinG: { ...EMPTY_DECIMAL_MACRO }, + fatG: { ...EMPTY_DECIMAL_MACRO }, + carbsG: { ...EMPTY_DECIMAL_MACRO }, + waterMl: { ...EMPTY_INT_MACRO }, + meals: [], + drinks: [], + }; + } + + const obj = raw as Record; + const meals = pick(obj, 'meals', 'Meals'); + const drinks = pick(obj, 'drinks', 'Drinks'); + + return { + date: String(pick(obj, 'date', 'Date') ?? ''), + goals: (pick(obj, 'goals', 'Goals') as DailySummary['goals']) ?? null, + calories: normalizeIntMacro(pick(obj, 'calories', 'Calories')), + proteinG: normalizeDecimalMacro(pick(obj, 'proteinG', 'ProteinG')), + fatG: normalizeDecimalMacro(pick(obj, 'fatG', 'FatG')), + carbsG: normalizeDecimalMacro(pick(obj, 'carbsG', 'CarbsG')), + waterMl: normalizeIntMacro(pick(obj, 'waterMl', 'WaterMl')), + meals: Array.isArray(meals) ? (meals as DailySummary['meals']) : [], + drinks: Array.isArray(drinks) ? (drinks as DailySummary['drinks']) : [], + }; +} + +export function formatReminderTime(value: string | null | undefined): string { + if (!value) return ''; + return value.length >= 5 ? value.slice(0, 5) : value; +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/ingredient-quantity.util.ts b/meal-plan-frontend-angular/src/app/core/utils/ingredient-quantity.util.ts new file mode 100644 index 0000000..c5f9124 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/ingredient-quantity.util.ts @@ -0,0 +1,112 @@ +const COUNT_AMOUNT_THRESHOLD = 10; + +const NON_QUANTIFIED_UNITS = new Set([ + 'to taste', + 'for serving', + 'optional', + 'do smaku', + 'do podania', + 'opcjonalnie', + 'garnish', + 'szczypta', +]); + +function normalizeUnit(unit: string): string { + return unit.trim().toLowerCase().replace(/\.$/, ''); +} + +export function isNonQuantifiedUnit(unit: string | null | undefined): boolean { + if (!unit?.trim()) return false; + return NON_QUANTIFIED_UNITS.has(normalizeUnit(unit)); +} + +function isCountUnit(unit: string | null | undefined): boolean { + if (!unit?.trim()) return false; + const u = normalizeUnit(unit); + return ( + u === 'szt' || + u === 'sztuka' || + u === 'sztuki' || + u === 'ząbek' || + u === 'zabek' || + u === 'zabki' || + u === 'clove' || + u === 'cloves' || + u === 'łyżka' || + u === 'lyzka' || + u === 'łyżeczka' || + u === 'lyzeczka' || + u === 'jajko' || + u === 'jajka' || + u === 'egg' || + u === 'eggs' + ); +} + +function gramsPerCountUnit(unit: string): number { + const u = normalizeUnit(unit); + switch (u) { + case 'ząbek': + case 'zabek': + case 'zabki': + case 'clove': + case 'cloves': + return 4; + case 'łyżka': + case 'lyzka': + return 15; + case 'łyżeczka': + case 'lyzeczka': + return 5; + default: + return 60; + } +} + +function roundAmount(value: number): number { + return Math.round(value * 10) / 10; +} + +export function normalizeIngredientQuantity( + amountGrams: number | null | undefined, + unit: string | null | undefined, +): { amountGrams: number | null; unit: string | null } { + const unitRaw = unit?.trim() || null; + + if (isNonQuantifiedUnit(unitRaw)) { + return { amountGrams: amountGrams ?? null, unit: unitRaw }; + } + + if (amountGrams == null || amountGrams <= 0) { + return { amountGrams: null, unit: unitRaw }; + } + + if (isCountUnit(unitRaw) && unitRaw) { + if (amountGrams <= COUNT_AMOUNT_THRESHOLD) { + return { + amountGrams: roundAmount(amountGrams * gramsPerCountUnit(unitRaw)), + unit: null, + }; + } + return { amountGrams: roundAmount(amountGrams), unit: null }; + } + + return { amountGrams: roundAmount(amountGrams), unit: null }; +} + +function formatNumber(value: number): string { + return Number.isInteger(value) ? value.toString() : value.toFixed(1).replace(/\.0$/, ''); +} + +export function formatIngredientAmount(amountGrams: number | null, unit: string | null): string { + if (isNonQuantifiedUnit(unit)) { + return unit?.trim() ?? ''; + } + + const normalized = normalizeIngredientQuantity(amountGrams, unit); + if (normalized.amountGrams == null || normalized.amountGrams <= 0) { + return normalized.unit ?? ''; + } + + return `${formatNumber(normalized.amountGrams)} g`; +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/meal-planner-suggestions.util.ts b/meal-plan-frontend-angular/src/app/core/utils/meal-planner-suggestions.util.ts new file mode 100644 index 0000000..36b6db2 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/meal-planner-suggestions.util.ts @@ -0,0 +1,98 @@ +import { MealPlanEntry } from '../models/meal-plan.models'; +import { MealCategory, RecipeListItem } from '../models/recipe.models'; + +export const PLANNER_MEAL_CATEGORIES = [ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, +] as const; + +export interface DayMealPlanning { + dailyTarget: number; + logged: number; + remaining: number; + slotTarget: number; + openSlots: number; + pendingCalories: number; +} + +export interface PendingMealSelection { + category: MealCategory; + calories: number; +} + +export function entryCaloriesTotal(entry: MealPlanEntry): number { + if (entry.calories == null) return 0; + return Math.round(entry.calories * entry.portions); +} + +export function computeDayMealPlanning( + date: string, + category: MealCategory, + entries: MealPlanEntry[], + dailyTarget: number, + pending?: PendingMealSelection | null, +): DayMealPlanning { + const dayEntries = entries.filter((e) => e.planDate === date); + const categoriesWithSaved = new Set(dayEntries.map((e) => e.mealCategory)); + const existingInSlot = dayEntries.find((e) => e.mealCategory === category); + + let logged = dayEntries.reduce((sum, e) => sum + entryCaloriesTotal(e), 0); + + const pendingCalories = + pending && pending.category === category && pending.calories > 0 ? pending.calories : 0; + + if (pendingCalories > 0) { + if (existingInSlot) { + logged = logged - entryCaloriesTotal(existingInSlot) + pendingCalories; + } else { + logged += pendingCalories; + } + } + + const remaining = dailyTarget - logged; + + const emptyCategories = PLANNER_MEAL_CATEGORIES.filter((cat) => !categoriesWithSaved.has(cat)); + const currentSlotSaved = categoriesWithSaved.has(category); + + let unfilled = [...emptyCategories]; + if (pendingCalories > 0 && !currentSlotSaved) { + unfilled = unfilled.filter((cat) => cat !== category); + } + + let openSlots: number; + if (currentSlotSaved) { + openSlots = Math.max(1, unfilled.length + 1); + } else if (pendingCalories > 0) { + openSlots = Math.max(1, unfilled.length); + } else if (unfilled.includes(category)) { + openSlots = Math.max(1, unfilled.length); + } else { + openSlots = Math.max(1, unfilled.length + 1); + } + + const slotTarget = dailyTarget > 0 ? Math.round(Math.max(0, remaining) / openSlots) : 0; + + return { dailyTarget, logged, remaining, slotTarget, openSlots, pendingCalories }; +} + +export function rankRecipeSuggestions( + recipes: RecipeListItem[], + category: MealCategory, + slotTarget: number, + limit = 6, +): { recipe: RecipeListItem; diff: number }[] { + if (slotTarget <= 0) return []; + + return recipes + .filter((r) => r.mealCategory === category && r.calories != null && r.calories > 0) + .map((recipe) => ({ recipe, diff: Math.abs(recipe.calories! - slotTarget) })) + .sort((a, b) => a.diff - b.diff || a.recipe.name.localeCompare(b.recipe.name)) + .slice(0, limit); +} + +export function estimateEntryCalories(recipe: RecipeListItem | null, portions: number): number | null { + if (!recipe?.calories) return null; + return Math.round(recipe.calories * portions); +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/pdf-export.util.ts b/meal-plan-frontend-angular/src/app/core/utils/pdf-export.util.ts new file mode 100644 index 0000000..a0be783 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/pdf-export.util.ts @@ -0,0 +1,159 @@ +import jsPDF from 'jspdf'; +import html2canvas from 'html2canvas'; +import { TranslateService } from '@ngx-translate/core'; +import { RecipeDetail } from '../models/recipe.models'; +import { isNonQuantifiedUnit, normalizeIngredientQuantity } from './ingredient-quantity.util'; + +export interface ShoppingItem { + name: string; + totalGrams: number | null; + totalAmount: number | null; + unit: string | null; + quantified: boolean; + sources: string[]; + contributions: Array<{ recipe: string; amount: number | null }>; +} + +function roundMacro(value: number): number { + return Math.round(value * 10) / 10; +} + +export function scaleRecipeDetail(recipe: RecipeDetail, portions: number): RecipeDetail { + return { + ...recipe, + calories: recipe.calories != null ? Math.round(recipe.calories * portions) : null, + protein: recipe.protein != null ? roundMacro(recipe.protein * portions) : null, + fat: recipe.fat != null ? roundMacro(recipe.fat * portions) : null, + carbs: recipe.carbs != null ? roundMacro(recipe.carbs * portions) : null, + ingredients: recipe.ingredients.map((ing) => ({ + ...ing, + amountGrams: ing.amountGrams != null ? roundMacro(ing.amountGrams * portions) : null, + })), + }; +} + +export function buildShoppingList(recipes: RecipeDetail[]): ShoppingItem[] { + const buckets = new Map(); + + for (const recipe of recipes) { + for (const ing of recipe.ingredients) { + const displayName = ing.name.trim(); + const normName = displayName.toLowerCase(); + const normalized = normalizeIngredientQuantity(ing.amountGrams, ing.unit); + const unitRaw = normalized.unit; + const nonQuantified = + isNonQuantifiedUnit(unitRaw) || (unitRaw === null && normalized.amountGrams === null); + + const key = nonQuantified ? `nq:${normName}` : `g:${normName}`; + + let bucket = buckets.get(key); + if (!bucket) { + bucket = { + name: displayName, + totalGrams: null, + totalAmount: null, + unit: nonQuantified ? unitRaw : null, + quantified: !nonQuantified, + sources: [], + contributions: [], + }; + buckets.set(key, bucket); + } + + if (!bucket.sources.includes(recipe.name)) { + bucket.sources.push(recipe.name); + } + + if (nonQuantified) { + continue; + } + + const amount = normalized.amountGrams; + if (amount == null || amount <= 0) { + continue; + } + + bucket.contributions.push({ recipe: recipe.name, amount }); + bucket.totalGrams = (bucket.totalGrams ?? 0) + amount; + } + } + + return Array.from(buckets.values()).sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), + ); +} + +function formatNumber(value: number): string { + return Number.isInteger(value) ? value.toString() : value.toFixed(2).replace(/\.?0+$/, ''); +} + +export function formatQuantity(item: ShoppingItem, toTasteLabel: string): string { + if (!item.quantified) { + return item.unit ? item.unit : toTasteLabel; + } + return `${formatNumber(item.totalGrams ?? 0)} g`; +} + +export function formatBreakdown(item: ShoppingItem): string | null { + if (!item.quantified || item.sources.length <= 1) { + return null; + } + + const amounts = item.contributions.map((c) => c.amount).filter((a): a is number => a !== null); + if (amounts.length === 0) { + return null; + } + + const unitLabel = 'g'; + const allEqual = amounts.every((a) => a === amounts[0]); + + if (allEqual && amounts.length > 1) { + return `${amounts.length} × ${formatNumber(amounts[0])} ${unitLabel}`; + } + + return amounts.map((a) => `${formatNumber(a)} ${unitLabel}`).join(' + '); +} + +export async function generatePdf( + translate: TranslateService, + fileName = 'meal-plan.pdf', + containerId = 'pdf-render-area', +): Promise { + const container = document.getElementById(containerId); + if (!container) { + throw new Error(translate.instant('errors.pdfRenderAreaMissing')); + } + + const pages = Array.from(container.querySelectorAll('.pdf-page')); + if (pages.length === 0) { + throw new Error(translate.instant('errors.pdfNothingToExport')); + } + + const pdf = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'portrait' }); + const pageWidth = pdf.internal.pageSize.getWidth(); + const pageHeight = pdf.internal.pageSize.getHeight(); + + for (let i = 0; i < pages.length; i += 1) { + const canvas = await html2canvas(pages[i], { + scale: 2, + backgroundColor: '#ffffff', + useCORS: true, + logging: false, + }); + + const imgData = canvas.toDataURL('image/jpeg', 0.92); + const imgWidth = pageWidth; + const imgHeight = (canvas.height * imgWidth) / canvas.width; + + if (i > 0) { + pdf.addPage(); + } + + const finalHeight = Math.min(imgHeight, pageHeight); + const finalWidth = + finalHeight === imgHeight ? imgWidth : (canvas.width * finalHeight) / canvas.height; + pdf.addImage(imgData, 'JPEG', (pageWidth - finalWidth) / 2, 0, finalWidth, finalHeight); + } + + pdf.save(fileName); +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/plural-translate.util.ts b/meal-plan-frontend-angular/src/app/core/utils/plural-translate.util.ts new file mode 100644 index 0000000..3bf4351 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/plural-translate.util.ts @@ -0,0 +1,29 @@ +import { TranslateService } from '@ngx-translate/core'; + +function polishPluralSuffix(count: number): string { + const mod10 = count % 10; + const mod100 = count % 100; + if (count === 1) return 'one'; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 'few'; + return 'many'; +} + +export function translatePlural( + translate: TranslateService, + key: string, + count: number, +): string { + const lang = translate.getCurrentLang() ?? translate.getFallbackLang() ?? 'en'; + let suffix: string; + if (lang.startsWith('pl')) { + suffix = polishPluralSuffix(count); + } else { + suffix = count === 1 ? 'one' : 'other'; + } + const fullKey = `${key}_${suffix}`; + const translated = translate.instant(fullKey, { count }); + if (translated === fullKey) { + return translate.instant(`${key}_other`, { count }); + } + return translated; +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/recipe-macros.util.ts b/meal-plan-frontend-angular/src/app/core/utils/recipe-macros.util.ts new file mode 100644 index 0000000..b2dfee4 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/recipe-macros.util.ts @@ -0,0 +1,62 @@ +import { IngredientNutritionItem } from '../models/ingredient-catalog.models'; + +export interface MacroTotals { + calories: number; + protein: number; + fat: number; + carbs: number; +} + +export interface MacroIngredientInput { + catalogItemId?: number | null; + amountGrams?: number | null; + unit?: string | null; + name?: string; +} + +export function hasNonMacroUnit(unit?: string | null): boolean { + if (!unit?.trim()) return false; + const u = unit.trim().toLowerCase(); + return u === 'do smaku' || u === 'optional' || u === 'opcjonalnie' || u === 'szczypta'; +} + +export function shouldIncludeIngredientInMacroSum(ing: MacroIngredientInput): boolean { + return Boolean( + ing.catalogItemId && ing.amountGrams != null && ing.amountGrams > 0 && !hasNonMacroUnit(ing.unit), + ); +} + +export function isUnlinkedMacroIngredient(ing: MacroIngredientInput): boolean { + if (!ing.name?.trim()) return false; + if (hasNonMacroUnit(ing.unit)) return false; + return Boolean(ing.amountGrams != null && ing.amountGrams > 0 && !ing.catalogItemId); +} + +export function countUnlinkedMacroIngredients(ingredients: MacroIngredientInput[]): number { + return ingredients.filter(isUnlinkedMacroIngredient).length; +} + +export function estimateMacrosFromCatalog( + ingredients: MacroIngredientInput[], + catalogItems: IngredientNutritionItem[], +): MacroTotals | null { + let calories = 0; + let protein = 0; + let fat = 0; + let carbs = 0; + let hasAny = false; + + for (const ing of ingredients) { + if (!shouldIncludeIngredientInMacroSum(ing)) continue; + const cat = catalogItems.find((c) => c.id === ing.catalogItemId); + if (!cat) continue; + const scale = Number(ing.amountGrams) / 100; + calories += Math.round(cat.caloriesPer100G * scale); + protein += cat.proteinGPer100G * scale; + fat += cat.fatGPer100G * scale; + carbs += cat.carbsGPer100G * scale; + hasAny = true; + } + + return hasAny ? { calories, protein, fat, carbs } : null; +} diff --git a/meal-plan-frontend-angular/src/app/core/utils/sort.util.ts b/meal-plan-frontend-angular/src/app/core/utils/sort.util.ts new file mode 100644 index 0000000..0b5135c --- /dev/null +++ b/meal-plan-frontend-angular/src/app/core/utils/sort.util.ts @@ -0,0 +1,53 @@ +export type SortDirection = 'asc' | 'desc'; + +export function nextSort( + current: { column: T; direction: SortDirection } | null, + column: T, +): { column: T; direction: SortDirection } { + if (current?.column === column) { + return { column, direction: current.direction === 'asc' ? 'desc' : 'asc' }; + } + return { column, direction: 'asc' }; +} + +export function sortIndicator( + current: { column: string; direction: SortDirection } | null, + column: string, +): string { + if (current?.column !== column) return ''; + return current.direction === 'asc' ? ' ↑' : ' ↓'; +} + +export function compareText(a: string, b: string, direction: SortDirection): number { + const result = a.localeCompare(b, undefined, { sensitivity: 'base', numeric: true }); + return direction === 'asc' ? result : -result; +} + +export function compareNumber( + a: number | null | undefined, + b: number | null | undefined, + direction: SortDirection, +): number { + const aMissing = a == null || Number.isNaN(a); + const bMissing = b == null || Number.isNaN(b); + if (aMissing && bMissing) return 0; + if (aMissing) return 1; + if (bMissing) return -1; + const result = a - b; + return direction === 'asc' ? result : -result; +} + +export function sortBy( + items: readonly T[], + direction: SortDirection, + getValue: (item: T) => string | number | null | undefined, +): T[] { + return [...items].sort((left, right) => { + const a = getValue(left); + const b = getValue(right); + if (typeof a === 'number' || typeof b === 'number') { + return compareNumber(typeof a === 'number' ? a : null, typeof b === 'number' ? b : null, direction); + } + return compareText(String(a ?? ''), String(b ?? ''), direction); + }); +} diff --git a/meal-plan-frontend-angular/src/app/features/all-meals/all-meals.component.ts b/meal-plan-frontend-angular/src/app/features/all-meals/all-meals.component.ts new file mode 100644 index 0000000..afce2b2 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/all-meals/all-meals.component.ts @@ -0,0 +1,234 @@ +import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Subject, debounceTime, distinctUntilChanged, takeUntil } from 'rxjs'; +import { RecipeService } from '../../core/services/recipe.service'; +import { RecipeListItem } from '../../core/models/recipe.models'; +import { RecipeCardComponent } from '../recipes/recipe-card.component'; +import { CategoryFilter, RecipeFiltersComponent } from '../recipes/recipe-filters.component'; +import { PdfGeneratorComponent } from '../pdf/pdf-generator.component'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { EmptyStateComponent } from '../../shared/components/empty-state.component'; +import { GridSkeletonComponent } from '../../shared/components/skeleton.component'; +import { translatePlural } from '../../core/utils/plural-translate.util'; +import { extractApiError } from '../../core/utils/api-error.util'; +import { nextSort, sortBy, sortIndicator, type SortDirection } from '../../core/utils/sort.util'; + +type RecipeSortColumn = 'name' | 'calories' | 'prepTime'; + +@Component({ + selector: 'app-all-meals', + standalone: true, + imports: [ + TranslatePipe, + RecipeCardComponent, + RecipeFiltersComponent, + PdfGeneratorComponent, + ErrorStateComponent, + EmptyStateComponent, + GridSkeletonComponent, + ], + template: ` +
+
+
+

{{ 'recipes.allMealsTitle' | translate }}

+

{{ 'recipes.allMealsSubtitle' | translate }}

+
+ + {{ selectedCountLabel() }} + +
+ + + +
+ + +
+ +
+
+ + @if (loading()) { + + } + + @if (error()) { + + } + + @if (!loading() && !error() && visible().length === 0) { + + } + + @if (!loading() && !error() && visible().length > 0) { +
+ @for (col of recipeSortColumns; track col.id) { + + } +
+
+ @for (recipe of sortedVisible(); track recipe.id) { + + } +
+ } +
+ `, +}) +export class AllMealsComponent implements OnInit, OnDestroy { + private readonly recipeService = inject(RecipeService); + private readonly translate = inject(TranslateService); + private readonly destroy$ = new Subject(); + private readonly searchDebounced$ = new Subject(); + + readonly filter = signal('all'); + readonly search = signal(''); + readonly debouncedSearch = signal(''); + readonly searchByIngredient = signal(false); + readonly selected = signal(new Set()); + readonly recipes = signal(null); + readonly loading = signal(true); + readonly error = signal(null); + readonly recipeSort = signal<{ column: RecipeSortColumn; direction: SortDirection }>({ + column: 'name', + direction: 'asc', + }); + readonly recipeSortColumns: { id: RecipeSortColumn; labelKey: string }[] = [ + { id: 'name', labelKey: 'manage.recipeName' }, + { id: 'calories', labelKey: 'recipes.calories' }, + { id: 'prepTime', labelKey: 'manage.prepTimeMin' }, + ]; + + readonly visible = computed(() => { + const list = this.recipes(); + if (!list) return []; + const term = this.search().trim().toLowerCase(); + const activeFilter = this.filter(); + return list.filter((r) => { + const matchesCategory = activeFilter === 'all' || r.mealCategory === activeFilter; + const matchesSearch = + this.searchByIngredient() || term === '' || r.name.toLowerCase().includes(term); + return matchesCategory && matchesSearch; + }); + }); + + readonly sortedVisible = computed(() => { + const { column, direction } = this.recipeSort(); + return sortBy(this.visible(), direction, (recipe) => { + switch (column) { + case 'name': + return recipe.name; + case 'calories': + return recipe.calories; + case 'prepTime': + return recipe.prepTimeMinutes; + } + }); + }); + + readonly selectedIds = computed(() => Array.from(this.selected())); + + ngOnInit(): void { + this.searchDebounced$ + .pipe(debounceTime(350), distinctUntilChanged(), takeUntil(this.destroy$)) + .subscribe((value) => { + this.debouncedSearch.set(value); + this.load(); + }); + this.load(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + onSearchChange(value: string): void { + this.search.set(value); + this.searchDebounced$.next(value.trim()); + } + + onIngredientModeChange(value: boolean): void { + this.searchByIngredient.set(value); + this.load(); + } + + load(): void { + this.loading.set(true); + this.error.set(null); + const ingredient = + this.searchByIngredient() && this.debouncedSearch() ? this.debouncedSearch() : undefined; + this.recipeService.getRecipes({ ingredient }).subscribe({ + next: (data) => { + this.recipes.set(data); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadRecipesFailed'))); + this.loading.set(false); + }, + }); + } + + toggleSelected(id: number): void { + this.selected.update((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + } + + selectAllVisible(): void { + this.selected.update((prev) => { + const next = new Set(prev); + this.visible().forEach((r) => next.add(r.id)); + return next; + }); + } + + deselectAll(): void { + this.selected.set(new Set()); + } + + selectedCountLabel(): string { + return translatePlural(this.translate, 'common.selectedCount', this.selected().size); + } + + toggleRecipeSort(column: RecipeSortColumn): void { + this.recipeSort.update((current) => nextSort(current, column)); + } + + recipeSortIndicator(column: RecipeSortColumn): string { + return sortIndicator(this.recipeSort(), column); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/auth/login.component.ts b/meal-plan-frontend-angular/src/app/features/auth/login.component.ts new file mode 100644 index 0000000..ef419c7 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/auth/login.component.ts @@ -0,0 +1,150 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../core/services/auth.service'; +import { ThemeService } from '../../core/services/theme.service'; +import { LanguageSwitcherComponent } from '../../shared/components/language-switcher.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, TranslatePipe, LanguageSwitcherComponent], + template: ` +
+
+ + +
+ +
+
+ + + +

{{ 'app.name' | translate }}

+

{{ 'auth.signInSubtitle' | translate }}

+
+ +
+ @if (error()) { + + } + +
+ + + @if (form.controls.email.touched && form.controls.email.errors?.['required']) { +

{{ 'validation.emailRequired' | translate }}

+ } + @if (form.controls.email.touched && form.controls.email.errors?.['email']) { +

{{ 'validation.emailInvalid' | translate }}

+ } +
+ +
+ + + @if (form.controls.password.touched && form.controls.password.errors?.['required']) { +

{{ 'validation.passwordRequired' | translate }}

+ } +
+ + + +

+ {{ 'auth.noAccount' | translate }} + + {{ 'auth.createOne' | translate }} + +

+
+
+
+ `, +}) +export class LoginComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly translate = inject(TranslateService); + readonly theme = inject(ThemeService); + + readonly pending = signal(false); + readonly error = signal(null); + + readonly form = this.fb.nonNullable.group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + + ngOnInit(): void { + if (this.auth.isAuthenticated()) { + void this.router.navigateByUrl('/'); + } + } + + async onSubmit(): Promise { + this.form.markAllAsTouched(); + if (this.form.invalid) return; + + this.pending.set(true); + this.error.set(null); + try { + await this.auth.login(this.form.getRawValue()); + void this.router.navigateByUrl('/'); + } catch (err) { + this.error.set(extractApiError(err, this.translate.instant('errors.signInFailed'))); + } finally { + this.pending.set(false); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/auth/register.component.ts b/meal-plan-frontend-angular/src/app/features/auth/register.component.ts new file mode 100644 index 0000000..bb4d317 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/auth/register.component.ts @@ -0,0 +1,198 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { + AbstractControl, + FormBuilder, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../core/services/auth.service'; +import { ThemeService } from '../../core/services/theme.service'; +import { LanguageSwitcherComponent } from '../../shared/components/language-switcher.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +function passwordStrength(control: AbstractControl): ValidationErrors | null { + const value = control.value as string; + if (!value) return null; + const errors: ValidationErrors = {}; + if (value.length < 8) errors['minLength'] = true; + if (!/[A-Z]/.test(value)) errors['uppercase'] = true; + if (!/[a-z]/.test(value)) errors['lowercase'] = true; + if (!/[0-9]/.test(value)) errors['digit'] = true; + return Object.keys(errors).length ? errors : null; +} + +function passwordsMatch(group: AbstractControl): ValidationErrors | null { + const password = group.get('password')?.value; + const confirm = group.get('confirmPassword')?.value; + return password === confirm ? null : { mismatch: true }; +} + +@Component({ + selector: 'app-register', + standalone: true, + imports: [ReactiveFormsModule, RouterLink, TranslatePipe, LanguageSwitcherComponent], + template: ` +
+
+ + +
+ +
+
+ + + +

{{ 'auth.createAccountTitle' | translate }}

+

{{ 'auth.createAccountSubtitle' | translate }}

+
+ +
+ @if (error()) { + + } + +
+ + + @if (form.controls.email.touched && form.controls.email.errors?.['required']) { +

{{ 'validation.emailRequired' | translate }}

+ } + @if (form.controls.email.touched && form.controls.email.errors?.['email']) { +

{{ 'validation.emailInvalid' | translate }}

+ } +
+ +
+ + + @if (form.controls.userName.touched && form.controls.userName.errors?.['maxlength']) { +

{{ 'validation.displayNameTooLong' | translate }}

+ } +
+ +
+ + + @if (form.controls.password.touched && form.controls.password.errors?.['minLength']) { +

{{ 'validation.passwordMinLength' | translate }}

+ } + @if (form.controls.password.touched && form.controls.password.errors?.['uppercase']) { +

{{ 'validation.passwordUppercase' | translate }}

+ } + @if (form.controls.password.touched && form.controls.password.errors?.['lowercase']) { +

{{ 'validation.passwordLowercase' | translate }}

+ } + @if (form.controls.password.touched && form.controls.password.errors?.['digit']) { +

{{ 'validation.passwordDigit' | translate }}

+ } +

{{ 'auth.passwordHint' | translate }}

+
+ +
+ + + @if (form.controls.confirmPassword.touched && form.controls.confirmPassword.errors?.['required']) { +

{{ 'validation.confirmPasswordRequired' | translate }}

+ } + @if (form.touched && form.errors?.['mismatch']) { +

{{ 'validation.passwordsMismatch' | translate }}

+ } +
+ + + +

+ {{ 'auth.hasAccount' | translate }} + + {{ 'auth.signIn' | translate }} + +

+
+
+
+ `, +}) +export class RegisterComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly translate = inject(TranslateService); + readonly theme = inject(ThemeService); + + readonly pending = signal(false); + readonly error = signal(null); + + readonly form = this.fb.nonNullable.group( + { + email: ['', [Validators.required, Validators.email]], + userName: ['', Validators.maxLength(256)], + password: ['', passwordStrength], + confirmPassword: ['', Validators.required], + }, + { validators: passwordsMatch }, + ); + + ngOnInit(): void { + if (this.auth.isAuthenticated()) { + void this.router.navigateByUrl('/'); + } + } + + async onSubmit(): Promise { + this.form.markAllAsTouched(); + if (this.form.invalid) return; + + this.pending.set(true); + this.error.set(null); + const { email, password, userName } = this.form.getRawValue(); + try { + await this.auth.register({ + email, + password, + userName: userName.trim() || undefined, + }); + void this.router.navigateByUrl('/'); + } catch (err) { + this.error.set(extractApiError(err, this.translate.instant('errors.registerFailed'))); + } finally { + this.pending.set(false); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/category/breakfast.component.ts b/meal-plan-frontend-angular/src/app/features/category/breakfast.component.ts new file mode 100644 index 0000000..dee409a --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/category/breakfast.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { MealCategory } from '../../core/models/recipe.models'; +import { CategoryPageComponent } from '../category/category-page.component'; + +@Component({ + selector: 'app-breakfast', + standalone: true, + imports: [CategoryPageComponent], + template: ``, +}) +export class BreakfastComponent { + readonly category = MealCategory.Breakfast; +} diff --git a/meal-plan-frontend-angular/src/app/features/category/category-page.component.ts b/meal-plan-frontend-angular/src/app/features/category/category-page.component.ts new file mode 100644 index 0000000..da88a2a --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/category/category-page.component.ts @@ -0,0 +1,159 @@ +import { Component, Input, OnInit, computed, inject, signal } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { RecipeService } from '../../core/services/recipe.service'; +import { + MealCategory, + MEAL_CATEGORY_I18N_KEYS, + RecipeListItem, +} from '../../core/models/recipe.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { EmptyStateComponent } from '../../shared/components/empty-state.component'; +import { GridSkeletonComponent } from '../../shared/components/skeleton.component'; +import { RecipeCardComponent } from '../recipes/recipe-card.component'; +import { translatePlural } from '../../core/utils/plural-translate.util'; +import { extractApiError } from '../../core/utils/api-error.util'; +import { nextSort, sortBy, sortIndicator, type SortDirection } from '../../core/utils/sort.util'; + +type RecipeSortColumn = 'name' | 'calories' | 'prepTime'; + +@Component({ + selector: 'app-category-page', + standalone: true, + imports: [ + TranslatePipe, + ErrorStateComponent, + EmptyStateComponent, + GridSkeletonComponent, + RecipeCardComponent, + ], + template: ` +
+
+

{{ categoryLabel | translate }}

+

+ @if (recipes()) { + {{ recipeCountLabel(recipes()!.length) }} + } @else { + {{ 'common.loading' | translate }} + } +

+
+ + @if (loading()) { + + } + + @if (error()) { + + } + + @if (!loading() && !error() && recipes()?.length === 0) { + + } + + @if (!loading() && !error() && recipes() && recipes()!.length > 0) { +
+ @for (col of recipeSortColumns; track col.id) { + + } +
+
+ @for (recipe of sortedRecipes(); track recipe.id) { + + } +
+ } +
+ `, +}) +export class CategoryPageComponent implements OnInit { + private readonly recipeService = inject(RecipeService); + private readonly route = inject(ActivatedRoute); + private readonly translate = inject(TranslateService); + + @Input() category?: MealCategory; + + readonly loading = signal(true); + readonly error = signal(null); + readonly recipes = signal(null); + readonly recipeSort = signal<{ column: RecipeSortColumn; direction: SortDirection }>({ + column: 'name', + direction: 'asc', + }); + readonly recipeSortColumns: { id: RecipeSortColumn; labelKey: string }[] = [ + { id: 'name', labelKey: 'manage.recipeName' }, + { id: 'calories', labelKey: 'recipes.calories' }, + { id: 'prepTime', labelKey: 'manage.prepTimeMin' }, + ]; + + readonly sortedRecipes = computed(() => { + const list = this.recipes(); + if (!list) return []; + const { column, direction } = this.recipeSort(); + return sortBy(list, direction, (recipe) => { + switch (column) { + case 'name': + return recipe.name; + case 'calories': + return recipe.calories; + case 'prepTime': + return recipe.prepTimeMinutes; + } + }); + }); + + get categoryLabel(): string { + const cat = this.resolvedCategory; + return cat != null ? MEAL_CATEGORY_I18N_KEYS[cat] : 'categories.unknown'; + } + + get emptyTitle(): string { + return this.translate.instant('recipes.emptyCategoryTitle', { + category: this.translate.instant(this.categoryLabel), + }); + } + + private get resolvedCategory(): MealCategory | undefined { + return this.category ?? this.route.snapshot.data['category']; + } + + ngOnInit(): void { + this.load(); + } + + load(): void { + const category = this.resolvedCategory; + if (category == null) return; + + this.loading.set(true); + this.error.set(null); + this.recipeService.getRecipes({ category }).subscribe({ + next: (data) => { + this.recipes.set(data); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadRecipesFailed'))); + this.loading.set(false); + }, + }); + } + + recipeCountLabel(count: number): string { + return translatePlural(this.translate, 'common.recipeCount', count); + } + + toggleRecipeSort(column: RecipeSortColumn): void { + this.recipeSort.update((current) => nextSort(current, column)); + } + + recipeSortIndicator(column: RecipeSortColumn): string { + return sortIndicator(this.recipeSort(), column); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/category/dinner.component.ts b/meal-plan-frontend-angular/src/app/features/category/dinner.component.ts new file mode 100644 index 0000000..6b7a7bf --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/category/dinner.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { MealCategory } from '../../core/models/recipe.models'; +import { CategoryPageComponent } from '../category/category-page.component'; + +@Component({ + selector: 'app-dinner', + standalone: true, + imports: [CategoryPageComponent], + template: ``, +}) +export class DinnerComponent { + readonly category = MealCategory.Dinner; +} diff --git a/meal-plan-frontend-angular/src/app/features/category/lunch.component.ts b/meal-plan-frontend-angular/src/app/features/category/lunch.component.ts new file mode 100644 index 0000000..9b80e4f --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/category/lunch.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { MealCategory } from '../../core/models/recipe.models'; +import { CategoryPageComponent } from '../category/category-page.component'; + +@Component({ + selector: 'app-lunch', + standalone: true, + imports: [CategoryPageComponent], + template: ``, +}) +export class LunchComponent { + readonly category = MealCategory.Lunch; +} diff --git a/meal-plan-frontend-angular/src/app/features/category/second-breakfast.component.ts b/meal-plan-frontend-angular/src/app/features/category/second-breakfast.component.ts new file mode 100644 index 0000000..4b43ba1 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/category/second-breakfast.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { MealCategory } from '../../core/models/recipe.models'; +import { CategoryPageComponent } from '../category/category-page.component'; + +@Component({ + selector: 'app-second-breakfast', + standalone: true, + imports: [CategoryPageComponent], + template: ``, +}) +export class SecondBreakfastComponent { + readonly category = MealCategory.SecondBreakfast; +} diff --git a/meal-plan-frontend-angular/src/app/features/dashboard/dashboard.component.ts b/meal-plan-frontend-angular/src/app/features/dashboard/dashboard.component.ts new file mode 100644 index 0000000..f781e76 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/dashboard/dashboard.component.ts @@ -0,0 +1,126 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../core/services/auth.service'; +import { RecipeService } from '../../core/services/recipe.service'; +import { + CategoryCount, + MealCategory, + MEAL_CATEGORY_I18N_KEYS, + MEAL_CATEGORY_ROUTES, +} from '../../core/models/recipe.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { translatePlural } from '../../core/utils/plural-translate.util'; +import { extractApiError } from '../../core/utils/api-error.util'; + +const CARD_META: Record< + MealCategory, + { emoji: string; gradient: string } +> = { + [MealCategory.Breakfast]: { emoji: '☕', gradient: 'from-amber-400 to-orange-500' }, + [MealCategory.SecondBreakfast]: { emoji: '🥐', gradient: 'from-sky-400 to-blue-500' }, + [MealCategory.Lunch]: { emoji: '🍲', gradient: 'from-brand-400 to-brand-600' }, + [MealCategory.Dinner]: { emoji: '🌙', gradient: 'from-violet-400 to-purple-600' }, +}; + +@Component({ + selector: 'app-dashboard', + standalone: true, + imports: [RouterLink, TranslatePipe, ErrorStateComponent, SkeletonComponent], + template: ` +
+
+

+ @if (auth.user()?.userName) { + {{ 'dashboard.welcomeNamed' | translate: { name: auth.user()!.userName } }} + } @else { + {{ 'dashboard.welcome' | translate }} + } +

+

{{ 'dashboard.subtitle' | translate }}

+
+ + @if (error()) { + + } @else { + + } +
+ `, +}) +export class DashboardComponent implements OnInit { + private readonly recipeService = inject(RecipeService); + private readonly translate = inject(TranslateService); + readonly auth = inject(AuthService); + + readonly loading = signal(true); + readonly error = signal(null); + readonly categories = signal([]); + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loading.set(true); + this.error.set(null); + this.recipeService.getCategories().subscribe({ + next: (data) => { + this.categories.set(data); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadCategoriesFailed'))); + this.loading.set(false); + }, + }); + } + + routeFor(category: MealCategory): string { + return MEAL_CATEGORY_ROUTES[category]; + } + + categoryLabel(category: MealCategory): string { + return MEAL_CATEGORY_I18N_KEYS[category]; + } + + metaFor(category: MealCategory) { + return CARD_META[category]; + } + + recipeCountLabel(count: number): string { + return translatePlural(this.translate, 'common.recipeCount', count); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/diet-generator/diet-generator.component.ts b/meal-plan-frontend-angular/src/app/features/diet-generator/diet-generator.component.ts new file mode 100644 index 0000000..1fc1d34 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/diet-generator/diet-generator.component.ts @@ -0,0 +1,420 @@ +import { Component, OnInit, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { switchMap } from 'rxjs'; +import { DietGeneratorService } from '../../core/services/diet-generator.service'; +import { + DEFAULT_DIET_PROFILE, + DietGeneratorDraftMeal, + DietGeneratorSession, + DietUserProfile, +} from '../../core/models/generator.models'; +import { MEAL_CATEGORY_I18N_KEYS, MealCategory, RecipeDetail } from '../../core/models/recipe.models'; +import { RecipeDetailViewComponent } from '../recipes/recipe-detail-view.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +type Step = 'profile' | 'macros' | 'plan' | 'done'; + +@Component({ + selector: 'app-diet-generator', + standalone: true, + imports: [FormsModule, RouterLink, TranslatePipe, RecipeDetailViewComponent], + template: ` +
+
+

{{ 'generators.diet.title' | translate }}

+

{{ 'generators.diet.subtitle' | translate }}

+
+ + @if (info()) { +
+ {{ info() }} +
+ } + + @if (error()) { + + } + + @if (step() === 'profile') { +
+

{{ 'generators.diet.profileTitle' | translate }}

+
+ + + + + + + +
+ +
+ } + + @if (step() === 'macros') { +
+

{{ 'generators.diet.macrosTitle' | translate }}

+ @if (macros.rationale) { +

{{ macros.rationale }}

+ } +
+ + + + +
+
+ + +
+
+ } + + @if (step() === 'plan' && session()) { +
+
+

{{ 'generators.diet.planHint' | translate }}

+
+ @for (entry of mealsByDate(); track entry.date) { +
+
+

{{ entry.date }}

+ + {{ entry.summary?.totalCalories ?? 0 }} / {{ entry.summary?.targetCalories ?? 0 }} kcal + @if (entry.summary?.withinTolerance) { ✓ } + +
+
    + @for (meal of entry.meals; track meal.id) { +
  • +
    +

    {{ categoryLabel(meal.mealCategory) | translate }}

    +

    {{ meal.recipeName }}

    +

    {{ mealMacroLine(meal) }}

    +
    +
    + + + +
    +
  • + } +
+
+ } +
+ +
+
+ } + + @if (step() === 'done') { +
+

{{ 'generators.diet.done' | translate }}

+ @if (commitResult()) { +

{{ commitResult() }}

+ } +
+ {{ 'nav.mealPlanner' | translate }} + +
+
+ } + + @if (loading() && step() !== 'done') { +

{{ 'common.loading' | translate }}

+ } + + @if (previewMeal()) { +
+ +
+ } +
+ `, +}) +export class DietGeneratorComponent implements OnInit { + private readonly dietGenerator = inject(DietGeneratorService); + private readonly translate = inject(TranslateService); + private readonly router = inject(Router); + + readonly step = signal('profile'); + readonly sessionId = signal(null); + readonly session = signal(null); + readonly loading = signal(false); + readonly error = signal(null); + readonly info = signal(null); + readonly commitResult = signal(null); + readonly previewMeal = signal(null); + + profile: DietUserProfile = { ...DEFAULT_DIET_PROFILE }; + macros = { calories: 2000, proteinG: 150, fatG: 65, carbsG: 200, rationale: '' }; + + readonly mealsByDate = computed(() => { + const s = this.session(); + if (!s) return []; + const map = new Map(); + for (const meal of s.draftMeals) { + const list = map.get(meal.planDate) ?? []; + list.push(meal); + map.set(meal.planDate, list); + } + return [...map.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, meals]) => ({ + date, + meals, + summary: s.daySummaries.find((d) => d.planDate === date), + })); + }); + + ngOnInit(): void { + this.dietGenerator.fetchProfile().subscribe({ + next: (p) => { + if (p) this.profile = { ...DEFAULT_DIET_PROFILE, ...p }; + }, + }); + } + + categoryLabel(category: number): string { + return MEAL_CATEGORY_I18N_KEYS[category as MealCategory] ?? 'categories.unknown'; + } + + mealMacroLine(meal: DietGeneratorDraftMeal): string { + const parts = [`${meal.calories ?? '?'} kcal`]; + if (meal.proteinG != null) parts.push(`P ${meal.proteinG}g`); + if (meal.fatG != null) parts.push(`F ${meal.fatG}g`); + if (meal.carbsG != null) parts.push(`C ${meal.carbsG}g`); + return parts.join(' · '); + } + + startSession(): void { + this.error.set(null); + this.loading.set(true); + this.dietGenerator + .saveProfile(this.profile) + .pipe( + switchMap(() => + this.dietGenerator.createSession({ planDayCount: 7, calorieToleranceKcal: 10 }), + ), + switchMap((session) => { + this.sessionId.set(session.id); + this.applyMacros(session); + return this.dietGenerator.proposeMacros(session.id); + }), + ) + .subscribe({ + next: (refined) => { + this.applyMacros(refined); + this.step.set('macros'); + this.loading.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.diet.error'))); + this.loading.set(false); + }, + }); + } + + acceptAndGenerate(): void { + const id = this.sessionId(); + if (!id) return; + this.error.set(null); + this.loading.set(true); + this.dietGenerator + .acceptMacros(id, this.macros) + .pipe(switchMap(() => this.dietGenerator.generatePlan(id))) + .subscribe({ + next: (plan) => { + this.session.set(plan); + this.step.set('plan'); + this.loading.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.diet.error'))); + this.loading.set(false); + }, + }); + } + + toggleLock(mealId: number, isLocked: boolean): void { + const id = this.sessionId(); + if (!id) return; + this.dietGenerator.toggleMealLock(id, mealId, isLocked).subscribe({ + next: (s) => this.session.set(s), + error: (e) => this.error.set(extractApiError(e, this.translate.instant('generators.diet.error'))), + }); + } + + regenerateMeal(planDate: string, mealCategory: number): void { + const id = this.sessionId(); + if (!id) return; + this.dietGenerator.regenerateMeal(id, planDate, mealCategory).subscribe({ + next: (s) => { + this.session.set(s); + this.error.set(null); + this.info.set(null); + }, + error: (e) => { + if (typeof e === 'object' && e !== null && 'status' in e && (e as { status: number }).status === 400) { + this.info.set(this.translate.instant('generators.diet.noAlternativeMeals')); + this.error.set(null); + } else { + this.error.set(extractApiError(e, this.translate.instant('generators.diet.error'))); + this.info.set(null); + } + }, + }); + } + + openRecipePreview(meal: DietGeneratorDraftMeal): void { + this.previewMeal.set(meal); + } + + closeRecipePreview(): void { + this.previewMeal.set(null); + } + + mealToRecipeDetail(meal: DietGeneratorDraftMeal): RecipeDetail { + return { + id: meal.recipeId, + name: meal.recipeName, + mealCategory: meal.mealCategory, + calories: meal.calories ?? null, + protein: meal.proteinG ?? null, + fat: meal.fatG ?? null, + carbs: meal.carbsG ?? null, + prepTimeMinutes: meal.prepTimeMinutes ?? null, + ingredients: (meal.ingredients ?? []).map((ingredient) => ({ + id: ingredient.id, + name: ingredient.name, + amountGrams: ingredient.amountGrams ?? null, + unit: ingredient.unit ?? null, + sortOrder: ingredient.sortOrder, + catalogItemId: null, + })), + steps: (meal.steps ?? []).map((step) => ({ + id: step.id, + stepNumber: step.stepNumber, + description: step.description, + })), + }; + } + + commitPlan(): void { + const id = this.sessionId(); + if (!id) return; + this.loading.set(true); + this.dietGenerator.commitPlan(id).subscribe({ + next: (r) => { + this.commitResult.set(`${r.planStartDate} → ${r.planEndDate}`); + this.step.set('done'); + this.loading.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.diet.error'))); + this.loading.set(false); + }, + }); + } + + reset(): void { + this.step.set('profile'); + this.sessionId.set(null); + this.session.set(null); + this.commitResult.set(null); + this.error.set(null); + void this.router.navigateByUrl('/diet-generator'); + } + + private applyMacros(session: DietGeneratorSession): void { + if (!session.proposedMacros) return; + this.macros = { + calories: session.proposedMacros.calories, + proteinG: session.proposedMacros.proteinG, + fatG: session.proposedMacros.fatG, + carbsG: session.proposedMacros.carbsG, + rationale: session.proposedMacros.rationale ?? '', + }; + } +} diff --git a/meal-plan-frontend-angular/src/app/features/diet-tracker/diet-tracker.component.ts b/meal-plan-frontend-angular/src/app/features/diet-tracker/diet-tracker.component.ts new file mode 100644 index 0000000..07ee311 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/diet-tracker/diet-tracker.component.ts @@ -0,0 +1,904 @@ +import { DecimalPipe } from '@angular/common'; +import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Subject, debounceTime, distinctUntilChanged, of, switchMap, takeUntil } from 'rxjs'; +import { DietService } from '../../core/services/diet.service'; +import { IngredientCatalogService } from '../../core/services/ingredient-catalog.service'; +import { LanguageService } from '../../core/services/language.service'; +import { RecipeService } from '../../core/services/recipe.service'; +import { + CONSUMPTION_CATEGORIES, + DailySummary, + DietReminder, + DietReminderType, + DietSuggestion, + FavoriteCatalogItem, + FavoriteRecipe, + HistoryDay, + LogMealInput, + MealPreview, + RecentMeal, + REMINDER_DAYS_MASK_ALL, + UserDailyGoals, + addDaysIso, + todayIso, +} from '../../core/models/diet.models'; +import { IngredientNutritionItem } from '../../core/models/ingredient-catalog.models'; +import { RecipeListItem } from '../../core/models/recipe.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { extractApiError } from '../../core/utils/api-error.util'; +import { formatReminderTime } from '../../core/utils/diet-normalize.util'; +import { nextSort, sortBy, sortIndicator, type SortDirection } from '../../core/utils/sort.util'; + +type MealLogSortColumn = 'name' | 'calories'; +type DrinkLogSortColumn = 'name' | 'volume'; +type ReminderSortColumn = 'title' | 'time'; +type Tab = 'today' | 'goals' | 'reminders' | 'history'; +type HistoryRange = 7 | 30; + +@Component({ + selector: 'app-diet-tracker', + standalone: true, + imports: [DecimalPipe, FormsModule, TranslatePipe, ErrorStateComponent, SkeletonComponent], + template: ` +
+
+

{{ 'diet.title' | translate }}

+

{{ 'diet.subtitle' | translate }}

+
+ +
+ @for (t of tabs; track t.id) { + + } +
+ + @if (tab() === 'today') { +
+ + + + {{ selectedDate() }} + @if (selectedDate() !== todayIso()) { + ({{ 'diet.notToday' | translate }}) + } +
+ + @if (loading()) { + + } @else if (error()) { + + } @else if (summary()) { +
+

{{ 'diet.quickLog' | translate }}

+
+ +
+ @if (recentMeals().length > 0) { +
+

{{ 'diet.recentMeals' | translate }}

+
+ @for (m of recentMeals(); track m.name + m.lastLoggedAt) { + + } +
+
+ } + @if (favoriteRecipes().length > 0 || favoriteCatalog().length > 0) { +
+

{{ 'diet.favorites' | translate }}

+
+ @for (f of favoriteRecipes(); track f.recipeId) { + + } + @for (f of favoriteCatalog(); track f.catalogItemId) { + + } +
+
+ } +
+ +
+ @for (card of progressCards(); track card.labelKey) { +
+

{{ card.labelKey | translate }}

+

+ {{ card.consumed }} + @if (card.goal != null) { + / {{ card.goal }}{{ card.unit }} + } +

+ @if (card.remaining != null) { +

{{ 'diet.remaining' | translate }}: {{ card.remaining }}{{ card.unit }}

+ } + @if (card.goal != null) { +
+
+
+ } +
+ } +
+ + @if (suggestions().length > 0) { +
+

{{ 'diet.suggestionsTitle' | translate }}

+

{{ 'diet.suggestionsHint' | translate }}

+
    + @for (s of suggestions(); track s.catalogItemId) { +
  • +
    +

    {{ suggestionName(s) }}

    +

    + {{ s.suggestedGrams }}g · {{ s.calories ?? '?' }} kcal · P {{ s.proteinG | number: '1.0-1' }}g + · {{ ('diet.suggestionReasons.' + s.reasonKey) | translate }} +

    +
    + +
  • + } +
+
+ } + +
+

{{ 'diet.logDrink' | translate }}

+
+ @for (item of catalog(); track item.id) { + + } +
+
+ +
+

{{ 'diet.logMeal' | translate }}

+ + + @if (recipeResults().length > 0) { +
    + @for (r of recipeResults(); track r.id) { +
  • + +
  • + } +
+ } +
+ + +
+ @if (catalogItemId) { + + } + + + @if (mealPreview()) { +
+

{{ 'diet.mealPreview' | translate }}: {{ mealPreview()!.meal.name }}

+

+ {{ mealPreview()!.meal.calories ?? '?' }} kcal · + P {{ (mealPreview()!.meal.proteinG ?? 0) | number: '1.0-1' }}g · + F {{ (mealPreview()!.meal.fatG ?? 0) | number: '1.0-1' }}g · + C {{ (mealPreview()!.meal.carbsG ?? 0) | number: '1.0-1' }}g +

+

+ {{ 'diet.remainingAfter' | translate }}: + {{ mealPreview()!.remainingAfter.calories ?? '?' }} kcal, + P {{ mealPreview()!.remainingAfter.proteinG ?? '?' }}g, + F {{ mealPreview()!.remainingAfter.fatG ?? '?' }}g, + C {{ mealPreview()!.remainingAfter.carbsG ?? '?' }}g +

+
+ } + + +
+ +
+
+

{{ mealsHeading() }}

+ @if ((summary()?.meals?.length ?? 0) > 0) { +
+ + + +
+ } +
    + @if ((summary()?.meals?.length ?? 0) === 0) { +
  • {{ 'diet.nothingLogged' | translate }}
  • + } @else { + @for (m of sortedMeals(); track m.id) { +
  • +
    +

    {{ m.name }}

    +

    {{ mealMacroLine(m) }}

    +
    + +
  • + } + } +
+
+
+

{{ drinksHeading() }}

+ @if ((summary()?.drinks?.length ?? 0) > 0) { +
+ + + +
+ } +
    + @if ((summary()?.drinks?.length ?? 0) === 0) { +
  • {{ 'diet.nothingLogged' | translate }}
  • + } @else { + @for (d of sortedDrinks(); track d.id) { +
  • +
    +

    {{ d.name }}

    +

    {{ d.volumeMl }} ml

    +
    + +
  • + } + } +
+
+
+ } + } + + @if (tab() === 'history') { +
+ + +
+ @if (historyLoading()) { + + } @else if (history().length === 0) { +

{{ 'diet.noHistory' | translate }}

+ } @else { +
+

{{ 'diet.historyCalories' | translate }}

+
+ @for (day of history(); track day.date) { +
+
+ {{ dayLabel(day.date) }} +
+ } +
+

{{ 'diet.historyMax' | translate: { max: historyMaxCalories() } }}

+
+ } + } + + @if (tab() === 'goals') { +
+

{{ 'diet.dailyGoals' | translate }}

+

{{ 'diet.goalsHint' | translate }}

+ @for (field of goalFields; track field.key) { + + } + +
+ } + + @if (tab() === 'reminders') { +
+
+

{{ editingReminderId() ? ('diet.editReminder' | translate) : ('diet.addReminder' | translate) }}

+ + + +
+ + @if (editingReminderId()) { + + } +
+
+
+

{{ 'diet.yourReminders' | translate }}

+ @if (reminders().length > 0) { +
+ + + +
+ } +
    + @if (reminders().length === 0) { +
  • {{ 'diet.noReminders' | translate }}
  • + } @else { + @for (r of sortedReminders(); track r.id) { +
  • +
    +

    {{ r.title }}

    +

    {{ formatReminderTime(r.timeOfDay) }}

    +
    +
    + + +
    +
  • + } + } +
+
+
+ } +
+ `, +}) +export class DietTrackerComponent implements OnInit, OnDestroy { + private readonly diet = inject(DietService); + private readonly recipes = inject(RecipeService); + private readonly ingredients = inject(IngredientCatalogService); + private readonly translate = inject(TranslateService); + private readonly language = inject(LanguageService); + private readonly destroy$ = new Subject(); + private readonly recipeSearch$ = new Subject(); + private readonly preview$ = new Subject(); + + readonly categories = CONSUMPTION_CATEGORIES; + readonly reminderTypes = DietReminderType; + readonly todayIso = todayIso; + readonly tabs: { id: Tab; labelKey: string }[] = [ + { id: 'today', labelKey: 'diet.tabToday' }, + { id: 'history', labelKey: 'diet.tabHistory' }, + { id: 'goals', labelKey: 'diet.tabGoals' }, + { id: 'reminders', labelKey: 'diet.tabReminders' }, + ]; + readonly goalFields: { key: keyof UserDailyGoals; labelKey: string }[] = [ + { key: 'calorieGoal', labelKey: 'diet.calorieGoal' }, + { key: 'proteinGoalG', labelKey: 'manage.proteinG' }, + { key: 'fatGoalG', labelKey: 'manage.fatG' }, + { key: 'carbsGoalG', labelKey: 'manage.carbsG' }, + { key: 'waterGoalMl', labelKey: 'diet.waterGoal' }, + ]; + + tab = signal('today'); + selectedDate = signal(todayIso()); + loading = signal(false); + busy = signal(false); + error = signal(null); + summary = signal(null); + catalog = signal<{ id: number; name: string; defaultVolumeMl: number; iconEmoji?: string | null }[]>([]); + recipeResults = signal([]); + reminders = signal([]); + suggestions = signal([]); + ingredientItems = signal([]); + mealPreview = signal(null); + recentMeals = signal([]); + favoriteRecipes = signal([]); + favoriteCatalog = signal([]); + history = signal([]); + historyLoading = signal(false); + historyRange = signal(7); + editingReminderId = signal(null); + + mealCategory = 2; + recipeSearch = ''; + selectedRecipeId: number | null = null; + customMealName = ''; + portions = 1; + catalogItemId: number | null = null; + grams = 150; + goalsForm: UserDailyGoals = {}; + reminderType = DietReminderType.Water; + reminderTitle = ''; + reminderTime = '09:00'; + + readonly mealSort = signal<{ column: MealLogSortColumn; direction: SortDirection }>({ + column: 'name', + direction: 'asc', + }); + readonly drinkSort = signal<{ column: DrinkLogSortColumn; direction: SortDirection }>({ + column: 'name', + direction: 'asc', + }); + readonly reminderSort = signal<{ column: ReminderSortColumn; direction: SortDirection }>({ + column: 'time', + direction: 'asc', + }); + + readonly sortedMeals = computed(() => { + const meals = this.summary()?.meals ?? []; + const { column, direction } = this.mealSort(); + return sortBy(meals, direction, (meal) => (column === 'name' ? meal.name : meal.calories)); + }); + + readonly sortedDrinks = computed(() => { + const drinks = this.summary()?.drinks ?? []; + const { column, direction } = this.drinkSort(); + return sortBy(drinks, direction, (drink) => (column === 'name' ? drink.name : drink.volumeMl)); + }); + + readonly sortedReminders = computed(() => { + const { column, direction } = this.reminderSort(); + return sortBy(this.reminders(), direction, (reminder) => + column === 'title' ? reminder.title : reminder.timeOfDay, + ); + }); + + readonly historyMaxCalories = computed(() => { + const days = this.history(); + if (days.length === 0) return 0; + return Math.max(...days.map((d) => d.calories), 1); + }); + + ngOnInit(): void { + this.loadSummary(); + this.loadQuickLogData(); + this.diet.getDrinkCatalog().subscribe({ next: (items) => this.catalog.set(items), error: () => this.catalog.set([]) }); + this.ingredients.getItems().subscribe({ next: (items) => this.ingredientItems.set(items), error: () => this.ingredientItems.set([]) }); + + this.recipeSearch$.pipe(debounceTime(300), distinctUntilChanged(), switchMap((q) => (q.trim().length >= 2 ? this.recipes.getRecipes({ search: q }) : of([]))), takeUntil(this.destroy$)).subscribe((items) => this.recipeResults.set(items)); + + this.preview$.pipe(debounceTime(250), takeUntil(this.destroy$)).subscribe(() => this.loadPreview()); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + readonly progressCards = computed(() => { + const s = this.summary(); + if (!s) return []; + const pct = (consumed: number, goal?: number | null) => (goal && goal > 0 ? Math.min((consumed / goal) * 100, 100) : 0); + return [ + { labelKey: 'diet.calories', consumed: s.calories.consumed, goal: s.calories.goal, remaining: s.calories.remaining, unit: ' kcal', percent: pct(s.calories.consumed, s.calories.goal) }, + { labelKey: 'diet.water', consumed: s.waterMl.consumed, goal: s.waterMl.goal, remaining: s.waterMl.remaining, unit: 'ml', percent: pct(s.waterMl.consumed, s.waterMl.goal) }, + { labelKey: 'recipes.protein', consumed: Number(s.proteinG.consumed), goal: s.proteinG.goal != null ? Number(s.proteinG.goal) : null, remaining: s.proteinG.remaining != null ? Number(s.proteinG.remaining) : null, unit: 'g', percent: pct(Number(s.proteinG.consumed), s.proteinG.goal != null ? Number(s.proteinG.goal) : null) }, + { labelKey: 'recipes.fat', consumed: Number(s.fatG.consumed), goal: s.fatG.goal != null ? Number(s.fatG.goal) : null, remaining: s.fatG.remaining != null ? Number(s.fatG.remaining) : null, unit: 'g', percent: pct(Number(s.fatG.consumed), s.fatG.goal != null ? Number(s.fatG.goal) : null) }, + { labelKey: 'recipes.carbs', consumed: Number(s.carbsG.consumed), goal: s.carbsG.goal != null ? Number(s.carbsG.goal) : null, remaining: s.carbsG.remaining != null ? Number(s.carbsG.remaining) : null, unit: 'g', percent: pct(Number(s.carbsG.consumed), s.carbsG.goal != null ? Number(s.carbsG.goal) : null) }, + ]; + }); + + formatReminderTime = formatReminderTime; + + mealsHeading(): string { + return this.selectedDate() === todayIso() + ? this.translate.instant('diet.mealsToday') + : this.translate.instant('diet.mealsOnDate', { date: this.selectedDate() }); + } + + drinksHeading(): string { + return this.selectedDate() === todayIso() + ? this.translate.instant('diet.drinksToday') + : this.translate.instant('diet.drinksOnDate', { date: this.selectedDate() }); + } + + shiftDate(days: number): void { + this.selectedDate.update((d) => addDaysIso(d, days)); + this.loadSummary(); + } + + goToday(): void { + this.selectedDate.set(todayIso()); + this.loadSummary(); + } + + setTab(id: Tab): void { + this.tab.set(id); + if (id === 'today') { + this.loadSummary(); + this.loadQuickLogData(); + } + if (id === 'goals') this.loadGoals(); + if (id === 'reminders') this.loadReminders(); + if (id === 'history') this.loadHistory(); + } + + setHistoryRange(range: HistoryRange): void { + this.historyRange.set(range); + this.loadHistory(); + } + + loadSummary(): void { + const date = this.selectedDate(); + this.loading.set(true); + this.error.set(null); + this.diet.getSummary(date).subscribe({ + next: (data) => { + this.summary.set(data); + this.loading.set(false); + this.loadSuggestions(); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadDietFailed'))); + this.summary.set(null); + this.loading.set(false); + }, + }); + } + + loadQuickLogData(): void { + this.diet.getRecentMeals(8).subscribe({ next: (m) => this.recentMeals.set(m), error: () => this.recentMeals.set([]) }); + this.diet.getFavoriteRecipes().subscribe({ next: (f) => this.favoriteRecipes.set(f), error: () => this.favoriteRecipes.set([]) }); + this.diet.getFavoriteCatalogItems().subscribe({ next: (f) => this.favoriteCatalog.set(f), error: () => this.favoriteCatalog.set([]) }); + } + + loadHistory(): void { + const to = todayIso(); + const from = addDaysIso(to, -(this.historyRange() - 1)); + this.historyLoading.set(true); + this.diet.getHistory(from, to).subscribe({ + next: (days) => { + this.history.set(days); + this.historyLoading.set(false); + }, + error: () => { + this.history.set([]); + this.historyLoading.set(false); + }, + }); + } + + barHeight(calories: number): number { + const max = this.historyMaxCalories(); + if (max <= 0) return 0; + return Math.max((calories / max) * 100, 2); + } + + dayLabel(iso: string): string { + const d = new Date(`${iso}T12:00:00`); + return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); + } + + loadSuggestions(): void { + this.diet.getSuggestions(this.selectedDate()).subscribe({ + next: (items) => this.suggestions.set(items), + error: () => this.suggestions.set([]), + }); + } + + loadGoals(): void { + this.diet.getGoals().subscribe((g) => (this.goalsForm = { ...g })); + } + + loadReminders(): void { + this.diet.getReminders().subscribe((items) => this.reminders.set(items)); + } + + onRecipeSearch(value: string): void { + this.recipeSearch = value; + this.selectedRecipeId = null; + this.catalogItemId = null; + this.mealPreview.set(null); + this.recipeSearch$.next(value); + } + + selectRecipe(id: number): void { + this.selectedRecipeId = id; + this.customMealName = ''; + this.catalogItemId = null; + this.refreshPreview(); + } + + onCustomMealChange(value: string): void { + this.customMealName = value; + if (value.trim()) { + this.selectedRecipeId = null; + this.catalogItemId = null; + } + this.refreshPreview(); + } + + onCatalogItemChange(id: number | null): void { + this.catalogItemId = id; + if (id) { + this.selectedRecipeId = null; + this.customMealName = ''; + } + this.refreshPreview(); + } + + refreshPreview(): void { + this.preview$.next(); + } + + buildMealPayload(): LogMealInput | null { + const date = this.selectedDate(); + if (this.selectedRecipeId) { + return { recipeId: this.selectedRecipeId, mealCategory: this.mealCategory, logDate: date, portions: this.portions }; + } + if (this.catalogItemId && this.grams > 0) { + return { catalogItemId: this.catalogItemId, grams: this.grams, mealCategory: this.mealCategory, logDate: date }; + } + if (this.customMealName.trim()) { + return { name: this.customMealName.trim(), mealCategory: this.mealCategory, logDate: date }; + } + return null; + } + + loadPreview(): void { + const payload = this.buildMealPayload(); + if (!payload) { + this.mealPreview.set(null); + return; + } + this.diet.previewMeal(payload, this.selectedDate()).subscribe({ + next: (preview) => this.mealPreview.set(preview), + error: () => this.mealPreview.set(null), + }); + } + + canLogMeal(): boolean { + return this.buildMealPayload() != null; + } + + logMeal(): void { + const payload = this.buildMealPayload(); + if (!payload) return; + this.busy.set(true); + this.diet.logMeal(payload).subscribe({ + next: () => { + this.resetMealForm(); + this.busy.set(false); + this.loadSummary(); + this.loadQuickLogData(); + }, + error: () => this.busy.set(false), + }); + } + + logRecent(m: RecentMeal): void { + this.busy.set(true); + const payload: LogMealInput = { + mealCategory: m.mealCategory, + logDate: this.selectedDate(), + portions: m.portions, + }; + if (m.recipeId) payload.recipeId = m.recipeId; + else if (m.catalogItemId && m.grams) { + payload.catalogItemId = m.catalogItemId; + payload.grams = m.grams; + } else payload.name = m.name; + this.diet.logMeal(payload).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + logFavoriteRecipe(f: FavoriteRecipe): void { + this.busy.set(true); + this.diet.logMeal({ recipeId: f.recipeId, mealCategory: f.mealCategory, logDate: this.selectedDate(), portions: 1 }).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + logFavoriteCatalog(f: FavoriteCatalogItem): void { + this.busy.set(true); + this.diet.logMeal({ catalogItemId: f.catalogItemId, grams: 150, mealCategory: this.mealCategory, logDate: this.selectedDate() }).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + copyYesterday(): void { + this.busy.set(true); + this.diet.copyYesterdayMeals(this.selectedDate()).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + logSuggestion(s: DietSuggestion): void { + this.busy.set(true); + this.diet.logMeal({ catalogItemId: s.catalogItemId, grams: s.suggestedGrams, mealCategory: this.mealCategory, logDate: this.selectedDate() }).subscribe({ + next: () => { + this.busy.set(false); + this.loadSummary(); + }, + error: () => this.busy.set(false), + }); + } + + resetMealForm(): void { + this.recipeSearch = ''; + this.customMealName = ''; + this.selectedRecipeId = null; + this.catalogItemId = null; + this.portions = 1; + this.grams = 150; + this.recipeResults.set([]); + this.mealPreview.set(null); + } + + logDrinkFromCatalog(drinkCatalogId: number, volumeMl: number): void { + this.busy.set(true); + this.diet.logDrink({ drinkCatalogId, volumeMl, logDate: this.selectedDate() }).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + removeMeal(id: number): void { + this.diet.deleteMeal(id).subscribe(() => this.loadSummary()); + } + + removeDrink(id: number): void { + this.diet.deleteDrink(id).subscribe(() => this.loadSummary()); + } + + saveGoals(): void { + this.busy.set(true); + this.diet.saveGoals(this.goalsForm).subscribe({ + next: () => { this.busy.set(false); this.loadSummary(); }, + error: () => this.busy.set(false), + }); + } + + startEditReminder(r: DietReminder): void { + this.editingReminderId.set(r.id); + this.reminderType = r.reminderType; + this.reminderTitle = r.title; + this.reminderTime = r.timeOfDay.slice(0, 5); + } + + cancelEditReminder(): void { + this.editingReminderId.set(null); + this.reminderTitle = ''; + this.reminderTime = '09:00'; + this.reminderType = DietReminderType.Water; + } + + saveReminder(): void { + this.busy.set(true); + const input = { + reminderType: this.reminderType, + title: this.reminderTitle.trim(), + timeOfDay: this.reminderTime, + daysOfWeekMask: REMINDER_DAYS_MASK_ALL, + }; + const editId = this.editingReminderId(); + const req = editId + ? this.diet.updateReminder(editId, input) + : this.diet.createReminder(input); + req.subscribe({ + next: () => { + this.cancelEditReminder(); + this.busy.set(false); + this.loadReminders(); + }, + error: () => this.busy.set(false), + }); + } + + removeReminder(id: number): void { + this.diet.deleteReminder(id).subscribe(() => this.loadReminders()); + } + + mealMacroLine(m: { calories?: number | null; proteinG?: number | null; fatG?: number | null; carbsG?: number | null }): string { + if (m.calories == null) return this.translate.instant('diet.noMacros'); + return `${m.calories} kcal · P ${Number(m.proteinG ?? 0).toFixed(1)}g · F ${Number(m.fatG ?? 0).toFixed(1)}g · C ${Number(m.carbsG ?? 0).toFixed(1)}g`; + } + + suggestionName(s: DietSuggestion): string { + if (this.language.currentLanguage() === 'pl' && s.namePl) return s.namePl; + return s.name; + } + + displayIngredientName(item: IngredientNutritionItem): string { + if (this.language.currentLanguage() === 'pl' && item.namePl) return item.namePl; + return item.name; + } + + favoriteCatalogName(f: FavoriteCatalogItem): string { + if (this.language.currentLanguage() === 'pl' && f.namePl) return f.namePl; + return f.name; + } + + toggleMealSort(column: MealLogSortColumn): void { + this.mealSort.update((current) => nextSort(current, column)); + } + + mealSortIndicator(column: MealLogSortColumn): string { + return sortIndicator(this.mealSort(), column); + } + + toggleDrinkSort(column: DrinkLogSortColumn): void { + this.drinkSort.update((current) => nextSort(current, column)); + } + + drinkSortIndicator(column: DrinkLogSortColumn): string { + return sortIndicator(this.drinkSort(), column); + } + + toggleReminderSort(column: ReminderSortColumn): void { + this.reminderSort.update((current) => nextSort(current, column)); + } + + reminderSortIndicator(column: ReminderSortColumn): string { + return sortIndicator(this.reminderSort(), column); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/ingredient-catalog/ingredient-catalog.component.ts b/meal-plan-frontend-angular/src/app/features/ingredient-catalog/ingredient-catalog.component.ts new file mode 100644 index 0000000..b30859f --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/ingredient-catalog/ingredient-catalog.component.ts @@ -0,0 +1,373 @@ +import { DecimalPipe } from '@angular/common'; +import { Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Subject, debounceTime, takeUntil } from 'rxjs'; +import { IngredientCatalogService } from '../../core/services/ingredient-catalog.service'; +import { LanguageService } from '../../core/services/language.service'; +import { + IngredientCategory, + IngredientNutritionItem, + UpsertIngredientNutritionItemInput, +} from '../../core/models/ingredient-catalog.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { EmptyStateComponent } from '../../shared/components/empty-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { extractApiError } from '../../core/utils/api-error.util'; +import { nextSort, sortBy, sortIndicator, type SortDirection } from '../../core/utils/sort.util'; + +type IngredientSortColumn = 'name' | 'category' | 'calories' | 'protein' | 'fat' | 'carbs' | 'fiber'; + +const EMPTY_FORM: UpsertIngredientNutritionItemInput = { + categoryId: 0, + name: '', + namePl: '', + caloriesPer100G: 0, + proteinGPer100G: 0, + fatGPer100G: 0, + carbsGPer100G: 0, + fiberGPer100G: null, + sortOrder: 0, + isActive: true, +}; + +@Component({ + selector: 'app-ingredient-catalog', + standalone: true, + imports: [DecimalPipe, FormsModule, TranslatePipe, ErrorStateComponent, EmptyStateComponent, SkeletonComponent], + template: ` +
+
+
+

+ {{ 'ingredientCatalog.title' | translate }} +

+

{{ 'ingredientCatalog.subtitle' | translate }}

+
+ +
+ + @if (showForm()) { +
+

+ {{ editingId() ? ('ingredientCatalog.editIngredient' | translate) : ('ingredientCatalog.addIngredient' | translate) }} +

+ @if (formError()) { +

{{ formError() }}

+ } +
+ + + + + + + + +
+
+ + +
+
+ } + +
+ + @for (cat of categories(); track cat.code) { + + } +
+ + + + @if (loading()) { + + } @else if (error()) { + + } @else if (items().length === 0) { + + } @else { +
+
+ + + + + + + + + + + + + + + @for (item of sortedItems(); track item.id) { + + + + + + + + + + + } + +
+ + + + + + + + + + + + + +
{{ displayName(item) }}{{ ('ingredientCatalog.categories.' + item.categoryCode) | translate }}{{ item.caloriesPer100G }}{{ item.proteinGPer100G | number: '1.1-1' }}{{ item.fatGPer100G | number: '1.1-1' }}{{ item.carbsGPer100G | number: '1.1-1' }}{{ item.fiberGPer100G != null ? (item.fiberGPer100G | number: '1.1-1') : '—' }} + + +
+
+

{{ 'ingredientCatalog.per100gNote' | translate }}

+
+ } +
+ `, +}) +export class IngredientCatalogComponent implements OnInit, OnDestroy { + private readonly catalog = inject(IngredientCatalogService); + private readonly translate = inject(TranslateService); + private readonly language = inject(LanguageService); + private readonly destroy$ = new Subject(); + private readonly search$ = new Subject(); + + categories = signal([]); + items = signal([]); + category = signal(''); + loading = signal(true); + error = signal(null); + showForm = signal(false); + editingId = signal(null); + saving = signal(false); + formError = signal(null); + search = ''; + form: UpsertIngredientNutritionItemInput = { ...EMPTY_FORM }; + sort = signal<{ column: IngredientSortColumn; direction: SortDirection }>({ column: 'name', direction: 'asc' }); + + readonly sortedItems = computed(() => { + const rows = this.items(); + const { column, direction } = this.sort(); + const lang = this.language.currentLanguage(); + return sortBy(rows, direction, (item) => { + switch (column) { + case 'name': + return lang === 'pl' && item.namePl ? item.namePl : item.name; + case 'category': + return item.categoryCode; + case 'calories': + return item.caloriesPer100G; + case 'protein': + return Number(item.proteinGPer100G); + case 'fat': + return Number(item.fatGPer100G); + case 'carbs': + return Number(item.carbsGPer100G); + case 'fiber': + return item.fiberGPer100G != null ? Number(item.fiberGPer100G) : null; + } + }); + }); + + ngOnInit(): void { + this.loadCategories(); + this.search$.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe(() => this.fetchItems()); + this.fetchItems(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + loadCategories(): void { + this.catalog.getCategories().subscribe({ + next: (cats) => this.categories.set(cats), + error: () => this.categories.set([]), + }); + } + + totalItems(): number { + return this.categories().reduce((sum, c) => sum + c.itemCount, 0); + } + + setCategory(code: string): void { + this.category.set(code); + this.fetchItems(); + } + + onSearch(value: string): void { + this.search = value; + this.search$.next(value); + } + + fetchItems(): void { + this.loading.set(true); + this.error.set(null); + this.catalog.getItems(this.category() || undefined, this.search.trim() || undefined).pipe(takeUntil(this.destroy$)).subscribe({ + next: (rows) => { + this.items.set(rows); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadIngredientCatalogFailed'))); + this.loading.set(false); + }, + }); + } + + openCreateForm(): void { + this.editingId.set(null); + this.form = { ...EMPTY_FORM, categoryId: this.categories()[0]?.id ?? 0 }; + this.formError.set(null); + this.showForm.set(true); + } + + openEditForm(item: IngredientNutritionItem): void { + this.editingId.set(item.id); + this.form = { + categoryId: item.categoryId, + name: item.name, + namePl: item.namePl ?? '', + caloriesPer100G: item.caloriesPer100G, + proteinGPer100G: item.proteinGPer100G, + fatGPer100G: item.fatGPer100G, + carbsGPer100G: item.carbsGPer100G, + fiberGPer100G: item.fiberGPer100G, + sortOrder: 0, + isActive: true, + }; + this.formError.set(null); + this.showForm.set(true); + } + + closeForm(): void { + this.showForm.set(false); + this.editingId.set(null); + this.formError.set(null); + } + + saveForm(): void { + if (!this.form.categoryId || !this.form.name.trim()) { + this.formError.set(this.translate.instant('ingredientCatalog.formInvalid')); + return; + } + this.saving.set(true); + this.formError.set(null); + const payload = { + ...this.form, + name: this.form.name.trim(), + namePl: this.form.namePl?.trim() || null, + fiberGPer100G: this.form.fiberGPer100G ?? null, + }; + const req = this.editingId() + ? this.catalog.updateItem(this.editingId()!, payload) + : this.catalog.createItem(payload); + req.pipe(takeUntil(this.destroy$)).subscribe({ + next: () => { + this.saving.set(false); + this.closeForm(); + this.loadCategories(); + this.fetchItems(); + }, + error: (err) => { + this.saving.set(false); + this.formError.set(extractApiError(err, this.translate.instant('errors.saveIngredientFailed'))); + }, + }); + } + + deleteItem(id: number): void { + if (!confirm(this.translate.instant('ingredientCatalog.deleteConfirm'))) return; + this.catalog.deleteItem(id).pipe(takeUntil(this.destroy$)).subscribe({ + next: () => { + this.loadCategories(); + this.fetchItems(); + }, + }); + } + + displayName(item: IngredientNutritionItem): string { + if (this.language.currentLanguage() === 'pl' && item.namePl) return item.namePl; + return item.name; + } + + toggleSort(column: IngredientSortColumn): void { + this.sort.update((current) => nextSort(current, column)); + } + + indicator(column: IngredientSortColumn): string { + return sortIndicator(this.sort(), column); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/manage-meals/excel-import-panel.component.ts b/meal-plan-frontend-angular/src/app/features/manage-meals/excel-import-panel.component.ts new file mode 100644 index 0000000..5259021 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/manage-meals/excel-import-panel.component.ts @@ -0,0 +1,146 @@ +import { Component, ElementRef, inject, signal, viewChild } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { firstValueFrom } from 'rxjs'; +import { RecipeManagementService } from '../../core/services/recipe-management.service'; +import { RecipeImportResult } from '../../core/models/recipe.models'; +import { extractApiError } from '../../core/utils/api-error.util'; + +@Component({ + selector: 'app-excel-import-panel', + standalone: true, + imports: [TranslatePipe], + template: ` +
+
+
+
+ 📊 +
+
+

{{ 'manage.importTitle' | translate }}

+

{{ 'manage.importDescription' | translate }}

+ +
+
+
+ +
+ + + + @if (importError()) { + + } + + @if (importResult()) { +
+
+ ✓ {{ 'manage.importComplete' | translate }} +
+

+ {{ 'manage.createdCount' | translate: { count: importResult()!.createdCount } }} · + {{ 'manage.skippedCount' | translate: { count: importResult()!.skippedCount } }} +

+ @if (importResult()!.errors.length > 0) { +
    + @for (msg of importResult()!.errors; track msg) { +
  • • {{ msg }}
  • + } +
+ } +
+ } + + +
+ +
+

{{ 'manage.excelFormatTitle' | translate }}

+
    +
  • {{ 'manage.excelRecipesSheet' | translate }}
  • +
  • {{ 'manage.excelIngredientsSheet' | translate }}
  • +
  • {{ 'manage.excelStepsSheet' | translate }}
  • +
+

{{ 'manage.duplicateSkipped' | translate }}

+
+
+ `, +}) +export class ExcelImportPanelComponent { + private readonly management = inject(RecipeManagementService); + private readonly translate = inject(TranslateService); + private readonly fileInput = viewChild>('fileInput'); + + readonly selectedFile = signal(null); + readonly downloading = signal(false); + readonly importPending = signal(false); + readonly importError = signal(null); + readonly importResult = signal(null); + + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + this.selectedFile.set(input.files?.[0] ?? null); + } + + async downloadTemplate(): Promise { + this.downloading.set(true); + try { + const blob = await firstValueFrom(this.management.downloadImportTemplate()); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'DailyMeals-recipe-import-template.xlsx'; + link.click(); + URL.revokeObjectURL(url); + } finally { + this.downloading.set(false); + } + } + + async handleImport(): Promise { + const file = this.selectedFile(); + if (!file) return; + + this.importError.set(null); + this.importResult.set(null); + this.importPending.set(true); + try { + const result = await firstValueFrom(this.management.importFromExcel(file)); + this.importResult.set(result); + this.selectedFile.set(null); + const input = this.fileInput()?.nativeElement; + if (input) input.value = ''; + } catch (err) { + this.importError.set(extractApiError(err, this.translate.instant('errors.importFailed'))); + } finally { + this.importPending.set(false); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/manage-meals/manage-meals.component.ts b/meal-plan-frontend-angular/src/app/features/manage-meals/manage-meals.component.ts new file mode 100644 index 0000000..a734c09 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/manage-meals/manage-meals.component.ts @@ -0,0 +1,267 @@ +import { Component, inject, signal, viewChild } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { firstValueFrom } from 'rxjs'; +import { CreateRecipeInput, RecipeListItem } from '../../core/models/recipe.models'; +import { RecipeManagementService } from '../../core/services/recipe-management.service'; +import { RecipeService } from '../../core/services/recipe.service'; +import { RecipeFormComponent } from './recipe-form.component'; +import { ExcelImportPanelComponent } from './excel-import-panel.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +type Tab = 'manual' | 'edit' | 'import'; + +@Component({ + selector: 'app-manage-meals', + standalone: true, + imports: [TranslatePipe, RecipeFormComponent, ExcelImportPanelComponent], + template: ` +
+
+
+

{{ 'manage.title' | translate }}

+

{{ 'manage.subtitle' | translate }}

+
+ +
+ + @if (recalculateError()) { + + } + +
+ + + +
+ + @if (successMessage() && (tab() === 'manual' || tab() === 'edit')) { +
+ ✓ {{ successMessage() }} +
+ } + + @if (tab() === 'manual') { + + } @else if (tab() === 'edit') { +
+
+ +
    + @if (loadingList()) { +
  • {{ 'common.loading' | translate }}
  • + } @else { + @for (r of filteredRecipes(); track r.id) { +
  • + +
  • + } + } +
+
+
+ @if (editingId()) { + + } @else { +

{{ 'manage.selectRecipeToEdit' | translate }}

+ } +
+
+ } @else { + + } +
+ `, +}) +export class ManageMealsComponent { + private readonly management = inject(RecipeManagementService); + private readonly recipes = inject(RecipeService); + private readonly translate = inject(TranslateService); + private readonly recipeForm = viewChild('recipeForm', { read: RecipeFormComponent }); + private readonly editForm = viewChild('editForm', { read: RecipeFormComponent }); + + readonly tab = signal('manual'); + readonly pending = signal(false); + readonly formError = signal(null); + readonly successMessage = signal(null); + readonly recipeList = signal([]); + readonly searchQuery = signal(''); + readonly loadingList = signal(false); + readonly editingId = signal(null); + readonly recalculatePending = signal(false); + readonly recalculateError = signal(null); + + readonly filteredRecipes = () => { + const q = this.searchQuery().trim().toLowerCase(); + const list = this.recipeList(); + if (!q) return list; + return list.filter((r) => r.name.toLowerCase().includes(q)); + }; + + setTab(id: Tab): void { + this.tab.set(id); + this.formError.set(null); + this.successMessage.set(null); + if (id === 'edit') this.loadRecipeList(); + } + + onSearch(event: Event): void { + this.searchQuery.set((event.target as HTMLInputElement).value); + } + + loadRecipeList(): void { + this.loadingList.set(true); + this.recipes.getRecipes().subscribe({ + next: (items) => { + this.recipeList.set(items); + this.loadingList.set(false); + }, + error: () => { + this.recipeList.set([]); + this.loadingList.set(false); + }, + }); + } + + async startEdit(id: number): Promise { + this.editingId.set(id); + this.formError.set(null); + this.successMessage.set(null); + try { + const recipe = await firstValueFrom(this.recipes.getRecipeById(id)); + setTimeout(() => this.editForm()?.loadRecipe(recipe)); + } catch (err) { + this.formError.set(extractApiError(err, this.translate.instant('errors.loadRecipeFailed'))); + } + } + + async handleCreate(input: CreateRecipeInput): Promise { + this.formError.set(null); + this.successMessage.set(null); + this.pending.set(true); + try { + const recipe = await firstValueFrom(this.management.createRecipe(input)); + this.successMessage.set(this.translate.instant('manage.savedSuccess', { name: recipe.name })); + this.recipeForm()?.reset(); + } catch (err) { + this.formError.set(extractApiError(err, this.translate.instant('errors.saveRecipeFailed'))); + } finally { + this.pending.set(false); + } + } + + async handleUpdate(input: CreateRecipeInput): Promise { + const id = this.editingId(); + if (!id) return; + this.formError.set(null); + this.successMessage.set(null); + this.pending.set(true); + try { + const recipe = await firstValueFrom(this.management.updateRecipe(id, input)); + this.successMessage.set(this.translate.instant('manage.updatedSuccess', { name: recipe.name })); + this.loadRecipeList(); + } catch (err) { + this.formError.set(extractApiError(err, this.translate.instant('errors.saveRecipeFailed'))); + } finally { + this.pending.set(false); + } + } + + async handleRecalculate(): Promise { + this.recalculateError.set(null); + this.successMessage.set(null); + this.recalculatePending.set(true); + try { + const result = await firstValueFrom(this.management.recalculateAllMacros()); + this.successMessage.set( + this.translate.instant('manage.recalculateSuccess', { + updated: result.recipesUpdated, + processed: result.recipesProcessed, + unlinked: result.unlinkedIngredientRows, + }), + ); + if (this.tab() === 'edit') { + this.loadRecipeList(); + const id = this.editingId(); + if (id) await this.startEdit(id); + } + } catch (err) { + this.recalculateError.set( + extractApiError(err, this.translate.instant('errors.recalculateMacrosFailed')), + ); + } finally { + this.recalculatePending.set(false); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/manage-meals/recipe-form.component.ts b/meal-plan-frontend-angular/src/app/features/manage-meals/recipe-form.component.ts new file mode 100644 index 0000000..2c6beba --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/manage-meals/recipe-form.component.ts @@ -0,0 +1,399 @@ +import { Component, EventEmitter, Input, Output, inject, OnInit } from '@angular/core'; +import { + AbstractControl, + FormArray, + FormBuilder, + ReactiveFormsModule, + ValidationErrors, + Validators, +} from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { + CreateRecipeInput, + MealCategory, + MEAL_CATEGORY_I18N_KEYS, + RecipeDetail, +} from '../../core/models/recipe.models'; +import { IngredientCatalogService } from '../../core/services/ingredient-catalog.service'; +import { IngredientNutritionItem } from '../../core/models/ingredient-catalog.models'; +import { LanguageService } from '../../core/services/language.service'; +import { + countUnlinkedMacroIngredients, + estimateMacrosFromCatalog, + hasNonMacroUnit, + isUnlinkedMacroIngredient, +} from '../../core/utils/recipe-macros.util'; + +const MEAL_CATEGORIES = [ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, +] as const; + +function minStepsArray(min: number) { + return (control: AbstractControl): ValidationErrors | null => { + const arr = control as FormArray; + return arr.length >= min ? null : { minSteps: true }; + }; +} + +@Component({ + selector: 'app-recipe-form', + standalone: true, + imports: [ReactiveFormsModule, TranslatePipe], + template: ` +
+ @if (error) { + + } + +
+

{{ 'manage.basicInfo' | translate }}

+
+
+ + + @if (form.controls.name.touched && form.controls.name.errors?.['required']) { +

{{ 'validation.recipeNameRequired' | translate }}

+ } +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ @if (estimatedTotalMacros(); as macros) { +

+ {{ 'manage.estimatedFromCatalog' | translate }}: + {{ macros.calories }} kcal · P {{ macros.protein.toFixed(1) }}g · F {{ macros.fat.toFixed(1) }}g · C + {{ macros.carbs.toFixed(1) }}g +

+ } + @if (unlinkedIngredientCount() > 0) { +

+ {{ 'manage.unlinkedIngredientsWarning' | translate: { count: unlinkedIngredientCount() } }} +

+ } +
+ +
+
+

{{ 'recipes.ingredients' | translate }}

+ +
+
+ @for (ctrl of ingredients.controls; track $index; let i = $index) { +
+
+ + + + +
+ + @if (macroHint(i); as hint) { +

{{ 'manage.macroHint' | translate: hint }}

+ } +
+ } +
+
+ +
+
+

{{ 'manage.preparationSteps' | translate }}

+ +
+ @if (steps.errors?.['minSteps']) { +

{{ 'validation.stepsMin' | translate }}

+ } +
+ @for (ctrl of steps.controls; track $index; let i = $index) { +
+ + + +
+ } +
+
+ + +
+ `, +}) +export class RecipeFormComponent implements OnInit { + private readonly fb = inject(FormBuilder); + private readonly translate = inject(TranslateService); + private readonly catalog = inject(IngredientCatalogService); + private readonly language = inject(LanguageService); + + @Input() pending = false; + @Input() error: string | null = null; + @Input() editMode = false; + @Input() recipeId: number | null = null; + @Output() submitRecipe = new EventEmitter(); + + readonly mealCategories = MEAL_CATEGORIES; + catalogItems: IngredientNutritionItem[] = []; + + readonly form = this.fb.nonNullable.group({ + name: ['', [Validators.required, Validators.maxLength(255)]], + mealCategory: [MealCategory.Breakfast], + calories: this.fb.control(null), + protein: this.fb.control(null), + fat: this.fb.control(null), + carbs: this.fb.control(null), + prepTimeMinutes: this.fb.control(null), + ingredients: this.fb.array([this.createIngredientGroup()]), + steps: this.fb.array([this.createStepGroup(1)], { validators: minStepsArray(1) }), + }); + + ngOnInit(): void { + this.catalog.getItems().subscribe({ + next: (items) => (this.catalogItems = items), + error: () => (this.catalogItems = []), + }); + } + + get ingredients(): FormArray { + return this.form.controls.ingredients; + } + + get steps(): FormArray { + return this.form.controls.steps; + } + + categoryLabel(cat: MealCategory): string { + return MEAL_CATEGORY_I18N_KEYS[cat]; + } + + displayCatalogName(item: IngredientNutritionItem): string { + if (this.language.currentLanguage() === 'pl' && item.namePl) return item.namePl; + return item.name; + } + + createIngredientGroup(catalogItemId: number | null = null) { + return this.fb.nonNullable.group({ + name: ['', Validators.required], + amountGrams: this.fb.control(null), + unit: [''], + sortOrder: [0], + catalogItemId: this.fb.control(catalogItemId), + }); + } + + createStepGroup(stepNumber: number) { + return this.fb.nonNullable.group({ + stepNumber: [stepNumber, [Validators.required, Validators.min(1)]], + description: ['', Validators.required], + }); + } + + addIngredient(): void { + this.ingredients.push( + this.fb.nonNullable.group({ + name: ['', Validators.required], + amountGrams: this.fb.control(null), + unit: [''], + sortOrder: [this.ingredients.length], + catalogItemId: this.fb.control(null), + }), + ); + } + + removeIngredient(index: number): void { + this.ingredients.removeAt(index); + } + + addStep(): void { + this.steps.push(this.createStepGroup(this.steps.length + 1)); + } + + removeStep(index: number): void { + this.steps.removeAt(index); + } + + onCatalogChange(index: number): void { + const group = this.ingredients.at(index); + const catalogItemId = group.get('catalogItemId')?.value as number | null; + if (!catalogItemId) return; + const item = this.catalogItems.find((c) => c.id === catalogItemId); + if (item && !group.get('name')?.value) { + group.patchValue({ name: this.displayCatalogName(item) }); + } + } + + onAmountChange(_index: number): void { + // triggers macroHint recomputation in template + } + + estimatedTotalMacros() { + const values = this.ingredients.getRawValue(); + return estimateMacrosFromCatalog(values, this.catalogItems); + } + + unlinkedIngredientCount(): number { + return countUnlinkedMacroIngredients(this.ingredients.getRawValue()); + } + + isUnlinkedIngredient(index: number): boolean { + const values = this.ingredients.at(index).getRawValue(); + return isUnlinkedMacroIngredient(values); + } + + macroHint(index: number): { calories: string; protein: string; fat: string; carbs: string } | null { + const group = this.ingredients.at(index); + const catalogItemId = group.get('catalogItemId')?.value as number | null; + const grams = group.get('amountGrams')?.value as number | null; + const unit = group.get('unit')?.value as string; + if (!catalogItemId || !grams || grams <= 0 || hasNonMacroUnit(unit)) return null; + const item = this.catalogItems.find((c) => c.id === catalogItemId); + if (!item) return null; + const factor = grams / 100; + return { + calories: (item.caloriesPer100G * factor).toFixed(0), + protein: (item.proteinGPer100G * factor).toFixed(1), + fat: (item.fatGPer100G * factor).toFixed(1), + carbs: (item.carbsGPer100G * factor).toFixed(1), + }; + } + + loadRecipe(recipe: RecipeDetail): void { + this.editMode = true; + this.recipeId = recipe.id; + this.form.patchValue({ + name: recipe.name, + mealCategory: recipe.mealCategory, + calories: recipe.calories, + protein: recipe.protein, + fat: recipe.fat, + carbs: recipe.carbs, + prepTimeMinutes: recipe.prepTimeMinutes, + }); + this.ingredients.clear(); + for (const ing of recipe.ingredients) { + this.ingredients.push( + this.fb.nonNullable.group({ + name: [ing.name, Validators.required], + amountGrams: this.fb.control(ing.amountGrams), + unit: [ing.unit ?? ''], + sortOrder: [ing.sortOrder], + catalogItemId: this.fb.control(ing.catalogItemId ?? null), + }), + ); + } + if (this.ingredients.length === 0) { + this.ingredients.push(this.createIngredientGroup()); + } + this.steps.clear(); + for (const step of recipe.steps) { + this.steps.push(this.createStepGroup(step.stepNumber)); + this.steps.at(this.steps.length - 1).patchValue({ + stepNumber: step.stepNumber, + description: step.description, + }); + } + if (this.steps.length === 0) { + this.steps.push(this.createStepGroup(1)); + } + } + + submit(): void { + this.form.markAllAsTouched(); + if (this.form.invalid) return; + + const values = this.form.getRawValue(); + this.submitRecipe.emit({ + name: values.name, + mealCategory: values.mealCategory, + calories: values.calories ?? null, + protein: values.protein ?? null, + fat: values.fat ?? null, + carbs: values.carbs ?? null, + prepTimeMinutes: values.prepTimeMinutes ?? null, + ingredients: values.ingredients.map((i, index) => ({ + name: i.name, + amountGrams: i.amountGrams ?? null, + unit: i.unit ?? '', + sortOrder: i.sortOrder ?? index, + catalogItemId: i.catalogItemId ?? null, + })), + steps: values.steps, + }); + } + + reset(): void { + this.editMode = false; + this.recipeId = null; + this.form.reset({ + name: '', + mealCategory: MealCategory.Breakfast, + calories: null, + protein: null, + fat: null, + carbs: null, + prepTimeMinutes: null, + }); + this.ingredients.clear(); + this.ingredients.push(this.createIngredientGroup()); + this.steps.clear(); + this.steps.push(this.createStepGroup(1)); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/meal-planner/meal-plan-day-pdf-export.component.ts b/meal-plan-frontend-angular/src/app/features/meal-planner/meal-plan-day-pdf-export.component.ts new file mode 100644 index 0000000..af9d05a --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/meal-planner/meal-plan-day-pdf-export.component.ts @@ -0,0 +1,261 @@ +import { Component, Input, inject, signal } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { forkJoin } from 'rxjs'; +import { RecipeService } from '../../core/services/recipe.service'; +import { + MealCategory, + MEAL_CATEGORY_I18N_KEYS, + RecipeDetail, +} from '../../core/models/recipe.models'; +import { MealPlanEntry } from '../../core/models/meal-plan.models'; +import { + buildShoppingList, + formatBreakdown, + formatQuantity, + generatePdf, + scaleRecipeDetail, + ShoppingItem, +} from '../../core/utils/pdf-export.util'; +import { formatIngredientAmount } from '../../core/utils/ingredient-quantity.util'; +import { extractApiError } from '../../core/utils/api-error.util'; + +const PLANNER_CATEGORIES = [ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, +] as const; + +type Phase = 'idle' | 'loading' | 'rendering'; + +interface ExportRecipe { + recipe: RecipeDetail; + slotLabel: string; + portions: number; +} + +const PAGE_STYLE: Record = { + width: '794px', + minHeight: '1123px', + boxSizing: 'border-box', + padding: '48px', + fontFamily: 'Inter, Arial, sans-serif', + color: '#0f172a', + background: '#ffffff', +}; + +@Component({ + selector: 'app-meal-plan-day-pdf-export', + standalone: true, + imports: [TranslatePipe], + template: ` + + + @if (error()) { +

{{ error() }}

+ } + + @if (phase() === 'rendering') { +
+
+
+
+

+ {{ 'pdf.dayPlanTitle' | translate: { date: dateLabel() } }} +

+
+
    + @for (slot of overviewSlots(); track slot.label) { +
  • + {{ slot.label }} + + @if (slot.recipeName) { + {{ slot.recipeName }} ({{ slot.portions }}×) + } @else { + {{ 'pdf.emptySlot' | translate }} + } + +
  • + } +
+
+
+ + @for (item of exportRecipes(); track item.recipe.id + item.slotLabel) { +
+
+
+ + {{ categoryLabel(item.recipe.mealCategory) }} + +

{{ item.recipe.name }}

+

+ {{ 'pdf.plannedMealMeta' | translate: { slot: item.slotLabel, portions: item.portions } }} +

+
+

{{ 'recipes.ingredients' | translate }}

+
    + @for (ing of item.recipe.ingredients; track ing.id) { +
  • + {{ ing.name }} + {{ formatIngredientAmount(ing.amountGrams, ing.unit) }} +
  • + } +
+

{{ 'recipes.preparation' | translate }}

+
    + @for (step of item.recipe.steps; track step.id) { +
  1. {{ step.description }}
  2. + } +
+
+
+ } + +
+
+
+

{{ 'pdf.shoppingList' | translate }}

+

+ {{ 'pdf.dayShoppingSubtitle' | translate: { date: dateLabel(), count: exportRecipes().length } }} +

+
+ @if (shoppingList().length === 0) { +

{{ 'pdf.emptyShoppingList' | translate }}

+ } @else { +
    + @for (item of shoppingList(); track item.name + (item.unit ?? 'g')) { +
  • + {{ item.name }} + — {{ formatQty(item) }} + @if (formatBreak(item); as breakdown) { + ({{ breakdown }}) + } +
  • + } +
+ } +
+
+
+ } + `, +}) +export class MealPlanDayPdfExportComponent { + private readonly recipes = inject(RecipeService); + private readonly translate = inject(TranslateService); + + @Input({ required: true }) date!: string; + @Input({ required: true }) entries: MealPlanEntry[] = []; + + readonly pageStyle = PAGE_STYLE; + readonly phase = signal('idle'); + readonly error = signal(null); + readonly exportRecipes = signal([]); + readonly shoppingList = signal([]); + readonly dateLabel = signal(''); + readonly overviewSlots = signal>([]); + + get renderAreaId(): string { + return `pdf-render-area-${this.date}`; + } + + categoryLabel(cat: MealCategory): string { + return this.translate.instant(MEAL_CATEGORY_I18N_KEYS[cat]); + } + + formatQty(item: ShoppingItem): string { + return formatQuantity(item, this.translate.instant('pdf.toTaste')); + } + + formatBreak(item: ShoppingItem): string | null { + return formatBreakdown(item); + } + + readonly formatIngredientAmount = formatIngredientAmount; + + handleGenerate(): void { + if (this.phase() !== 'idle') return; + this.error.set(null); + this.phase.set('loading'); + + const dayEntries = this.entries + .filter((e) => e.planDate === this.date) + .sort((a, b) => a.mealCategory - b.mealCategory || a.id - b.id); + + this.dateLabel.set( + new Date(`${this.date}T12:00:00`).toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', + day: 'numeric', + }), + ); + + this.overviewSlots.set( + PLANNER_CATEGORIES.map((cat) => { + const first = dayEntries.find((e) => e.mealCategory === cat); + return { + label: this.categoryLabel(cat), + recipeName: first?.recipeName ?? null, + portions: first?.portions ?? null, + }; + }), + ); + + if (dayEntries.length === 0) { + this.exportRecipes.set([]); + this.shoppingList.set([]); + this.phase.set('rendering'); + void this.runPdf(); + return; + } + + forkJoin(dayEntries.map((entry) => this.recipes.getRecipeById(entry.recipeId))).subscribe({ + next: (details) => { + const scaled = details.map((recipe, index) => { + const entry = dayEntries[index]; + return { + slotLabel: this.categoryLabel(entry.mealCategory as MealCategory), + portions: entry.portions, + recipe: scaleRecipeDetail(recipe, entry.portions), + }; + }); + this.exportRecipes.set(scaled); + this.shoppingList.set(buildShoppingList(scaled.map((s) => s.recipe))); + this.phase.set('rendering'); + void this.runPdf(); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.pdfLoadFailed'))); + this.phase.set('idle'); + }, + }); + } + + private async runPdf(): Promise { + await document.fonts?.ready?.catch(() => undefined); + await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); + try { + const safeDate = this.date.replace(/[^\d-]/g, ''); + await generatePdf(this.translate, `dailymeals-${safeDate}.pdf`, this.renderAreaId); + } catch (err) { + this.error.set(extractApiError(err, this.translate.instant('errors.pdfGenerateFailed'))); + } finally { + this.phase.set('idle'); + this.exportRecipes.set([]); + this.shoppingList.set([]); + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/meal-planner/meal-planner.component.ts b/meal-plan-frontend-angular/src/app/features/meal-planner/meal-planner.component.ts new file mode 100644 index 0000000..bb09672 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/meal-planner/meal-planner.component.ts @@ -0,0 +1,414 @@ +import { DecimalPipe } from '@angular/common'; +import { Component, OnInit, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Subject, debounceTime, distinctUntilChanged, of, switchMap, takeUntil } from 'rxjs'; +import { MealPlanService } from '../../core/services/meal-plan.service'; +import { RecipeService } from '../../core/services/recipe.service'; +import { DietService } from '../../core/services/diet.service'; +import { MealPlanEntry } from '../../core/models/meal-plan.models'; +import { MealCategory, MEAL_CATEGORY_I18N_KEYS, RecipeListItem } from '../../core/models/recipe.models'; +import { + computeDayMealPlanning, + estimateEntryCalories, + rankRecipeSuggestions, +} from '../../core/utils/meal-planner-suggestions.util'; +import { + addDaysIso, + endOfWeekIso, + startOfWeekIso, + todayIso, +} from '../../core/models/diet.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { MealPlanDayPdfExportComponent } from './meal-plan-day-pdf-export.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +const PLANNER_CATEGORIES = [ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, +] as const; + +@Component({ + selector: 'app-meal-planner', + standalone: true, + imports: [DecimalPipe, FormsModule, TranslatePipe, ErrorStateComponent, SkeletonComponent, MealPlanDayPdfExportComponent], + template: ` +
+
+

{{ 'mealPlanner.title' | translate }}

+

{{ 'mealPlanner.subtitle' | translate }}

+
+ +
+ + + + {{ weekLabel() }} +
+ + @if (loading()) { + + } @else if (error()) { + + } @else { +
+ + + + + @for (day of weekDays(); track day.iso) { + + } + + + + @for (cat of categories; track cat) { + + + @for (day of weekDays(); track day.iso) { + + } + + } + +
{{ 'common.category' | translate }} +
+ {{ day.label }} + +
+
{{ categoryLabel(cat) | translate }} + @for (entry of entriesFor(day.iso, cat); track entry.id) { +
+
+

{{ entry.recipeName }}

+

{{ entry.portions | number: '1.0-1' }}× · {{ entry.calories ?? '?' }} kcal

+
+ +
+ } + +
+
+ } + + @if (adding()) { +
+

{{ 'mealPlanner.addRecipe' | translate }}

+

{{ addingLabel() }}

+ + @if (dayPlanning() && dailyTarget() > 0) { +
+

{{ 'mealPlanner.daySummary' | translate: { logged: dayPlanning()!.logged, target: dayPlanning()!.dailyTarget } }}

+

+ {{ 'mealPlanner.remaining' | translate: { count: remainingCalories() } }} + @if (dayPlanning()!.slotTarget > 0) { + · {{ 'mealPlanner.slotHint' | translate: { count: dayPlanning()!.slotTarget } }} + } + @if (dayPlanning()!.pendingCalories > 0) { + · {{ 'mealPlanner.includingSelection' | translate: { count: dayPlanning()!.pendingCalories } }} + } +

+
+ } @else { +

{{ 'mealPlanner.noSuggestions' | translate }}

+ } + @if (suggestions().length > 0) { +
+

{{ 'mealPlanner.suggestionsTitle' | translate }}

+
    + @for (item of suggestions(); track item.recipe.id) { +
  • + +
  • + } +
+
+ } + + @if (recipeSearch.trim().length >= 2 && !selectedRecipe() && recipeResults().length > 0) { +
    + @for (r of recipeResults(); track r.id) { +
  • + +
  • + } +
+ } @else if (recipeSearch.trim().length >= 2 && !selectedRecipe() && recipeResults().length === 0) { +

{{ 'recipes.noMatchingTitle' | translate }}

+ } + @if (selectedRecipe()) { +
+ + {{ 'mealPlanner.selectedRecipe' | translate: { name: selectedRecipe()!.name } }} + @if (estimatedMealCalories() != null) { + · {{ estimatedMealCalories() }} kcal + } @else if (selectedRecipe()!.calories != null) { + · {{ selectedRecipe()!.calories }} kcal + } + + +
+ } + + @if (estimatedMealCalories() != null) { +

{{ 'mealPlanner.estimatedMeal' | translate: { count: estimatedMealCalories() } }}

+ } +
+ + +
+
+ } +
+ `, +}) +export class MealPlannerComponent implements OnInit { + private readonly mealPlan = inject(MealPlanService); + private readonly recipes = inject(RecipeService); + private readonly diet = inject(DietService); + private readonly translate = inject(TranslateService); + private readonly destroy$ = new Subject(); + private readonly recipeSearch$ = new Subject(); + + readonly categories = PLANNER_CATEGORIES; + readonly todayIso = todayIso; + + weekStart = signal(startOfWeekIso(todayIso())); + entries = signal([]); + loading = signal(false); + error = signal(null); + busy = signal(false); + adding = signal<{ date: string; category: MealCategory } | null>(null); + recipeSearch = ''; + selectedRecipe = signal(null); + recipeResults = signal([]); + categoryRecipes = signal([]); + calorieGoal = signal(0); + dailyTargets = signal>({}); + portions = 1; + + readonly dailyTarget = computed(() => { + const ctx = this.adding(); + if (!ctx) return 0; + return this.dailyTargets()[ctx.date] ?? this.calorieGoal(); + }); + + readonly estimatedMealCalories = computed(() => + estimateEntryCalories(this.selectedRecipe(), this.portions), + ); + + readonly dayPlanning = computed(() => { + const ctx = this.adding(); + if (!ctx) return null; + const pendingCalories = this.estimatedMealCalories(); + const pending = + pendingCalories != null && pendingCalories > 0 + ? { category: ctx.category, calories: pendingCalories } + : null; + return computeDayMealPlanning(ctx.date, ctx.category, this.entries(), this.dailyTarget(), pending); + }); + + readonly remainingCalories = computed(() => { + const planning = this.dayPlanning(); + return planning ? Math.max(0, planning.remaining) : 0; + }); + + readonly suggestions = computed(() => { + const ctx = this.adding(); + const planning = this.dayPlanning(); + if (!ctx || !planning) return []; + return rankRecipeSuggestions(this.categoryRecipes(), ctx.category, planning.slotTarget); + }); + + readonly weekDays = computed(() => { + const start = this.weekStart(); + return Array.from({ length: 7 }, (_, i) => { + const iso = addDaysIso(start, i); + const d = new Date(`${iso}T12:00:00`); + return { + iso, + label: d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }), + }; + }); + }); + + readonly weekLabel = computed(() => { + const start = this.weekStart(); + const end = endOfWeekIso(start); + return `${start} — ${end}`; + }); + + ngOnInit(): void { + this.loadEntries(); + this.diet.getGoals().subscribe((g) => this.calorieGoal.set(g.calorieGoal ?? 0)); + this.recipeSearch$ + .pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap((q) => { + if (q.trim().length >= 2) { + return this.recipes.getRecipes({ search: q }); + } + return of([]); + }), + takeUntil(this.destroy$), + ) + .subscribe((items) => this.recipeResults.set(items)); + } + + categoryLabel(cat: MealCategory): string { + return MEAL_CATEGORY_I18N_KEYS[cat]; + } + + entriesFor(date: string, category: MealCategory): MealPlanEntry[] { + return this.entries().filter((e) => e.planDate === date && e.mealCategory === category); + } + + loadEntries(): void { + const from = this.weekStart(); + const to = endOfWeekIso(from); + this.loading.set(true); + this.error.set(null); + this.mealPlan.getEntries(from, to).subscribe({ + next: (items) => { + this.entries.set(items); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadMealPlanFailed'))); + this.loading.set(false); + }, + }); + } + + prevWeek(): void { + this.weekStart.update((s) => addDaysIso(s, -7)); + this.loadEntries(); + } + + nextWeek(): void { + this.weekStart.update((s) => addDaysIso(s, 7)); + this.loadEntries(); + } + + goToday(): void { + this.weekStart.set(startOfWeekIso(todayIso())); + this.loadEntries(); + } + + openAdd(date: string, category: MealCategory): void { + this.adding.set({ date, category }); + this.recipeSearch = ''; + this.selectedRecipe.set(null); + this.recipeResults.set([]); + this.portions = 1; + this.recipes.getRecipes({ category }).subscribe((items) => this.categoryRecipes.set(items)); + } + + setDailyTarget(value: number): void { + const ctx = this.adding(); + if (!ctx) return; + const parsed = Number.isFinite(value) ? value : 0; + this.dailyTargets.update((prev) => ({ ...prev, [ctx.date]: parsed })); + } + + cancelAdd(): void { + this.adding.set(null); + } + + addingLabel(): string { + const ctx = this.adding(); + if (!ctx) return ''; + return `${ctx.date} · ${this.translate.instant(this.categoryLabel(ctx.category))}`; + } + + onRecipeSearch(value: string): void { + this.recipeSearch = value; + this.recipeSearch$.next(value); + } + + selectFromSuggestion(r: RecipeListItem): void { + this.selectedRecipe.set(r); + } + + selectFromSearch(r: RecipeListItem): void { + this.selectedRecipe.set(r); + this.recipeSearch = ''; + this.recipeResults.set([]); + } + + clearSelection(): void { + this.selectedRecipe.set(null); + } + + saveEntry(): void { + const ctx = this.adding(); + const recipe = this.selectedRecipe(); + if (!ctx || !recipe) return; + this.busy.set(true); + this.mealPlan + .upsertEntry({ + planDate: ctx.date, + mealCategory: ctx.category, + recipeId: recipe.id, + portions: this.portions, + }) + .subscribe({ + next: () => { + this.busy.set(false); + this.adding.set(null); + this.loadEntries(); + }, + error: () => this.busy.set(false), + }); + } + + removeEntry(id: number): void { + this.mealPlan.deleteEntry(id).subscribe(() => this.loadEntries()); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/not-found/not-found.component.ts b/meal-plan-frontend-angular/src/app/features/not-found/not-found.component.ts new file mode 100644 index 0000000..834b41c --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/not-found/not-found.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; + +@Component({ + selector: 'app-not-found', + standalone: true, + imports: [RouterLink, TranslatePipe], + template: ` +
+

404

+

{{ 'notFound.title' | translate }}

+

{{ 'notFound.description' | translate }}

+ {{ 'notFound.backToDashboard' | translate }} +
+ `, +}) +export class NotFoundComponent {} diff --git a/meal-plan-frontend-angular/src/app/features/pdf/pdf-generator.component.ts b/meal-plan-frontend-angular/src/app/features/pdf/pdf-generator.component.ts new file mode 100644 index 0000000..3ecbb5f --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/pdf/pdf-generator.component.ts @@ -0,0 +1,317 @@ +import { Component, Input, OnDestroy, effect, inject, signal } from '@angular/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { forkJoin } from 'rxjs'; +import { RecipeService } from '../../core/services/recipe.service'; +import { + MealCategory, + MEAL_CATEGORY_I18N_KEYS, + RecipeDetail, +} from '../../core/models/recipe.models'; +import { + buildShoppingList, + formatBreakdown, + formatQuantity, + generatePdf, + ShoppingItem, +} from '../../core/utils/pdf-export.util'; +import { formatIngredientAmount } from '../../core/utils/ingredient-quantity.util'; +import { extractApiError } from '../../core/utils/api-error.util'; + +type Phase = 'idle' | 'loading' | 'rendering'; + +const PAGE_STYLE: Record = { + width: '794px', + minHeight: '1123px', + boxSizing: 'border-box', + padding: '48px', + fontFamily: 'Inter, Arial, sans-serif', + color: '#0f172a', + background: '#ffffff', +}; + +@Component({ + selector: 'app-pdf-generator', + standalone: true, + imports: [TranslatePipe], + template: ` + + + @if (error()) { +

{{ error() }}

+ } + + @if (phase() === 'rendering') { +
+ @for (recipe of recipes(); track recipe.id) { +
+
+
+ + {{ categoryLabel(recipe.mealCategory) }} + +

{{ recipe.name }}

+ @if (recipe.prepTimeMinutes != null) { +

+ {{ 'pdf.prepTime' | translate: { count: recipe.prepTimeMinutes } }} +

+ } +
+ +
+ @for (macro of macros(recipe); track macro.label) { +
+
+ {{ macro.value != null ? macro.value + ' ' + macro.suffix : '—' }} +
+
+ {{ macro.label }} +
+
+ } +
+ +

{{ 'recipes.ingredients' | translate }}

+
    + @for (ing of recipe.ingredients; track ing.id) { +
  • + {{ ing.name }} + {{ formatIngredientAmount(ing.amountGrams, ing.unit) }} +
  • + } +
+ +

{{ 'recipes.preparation' | translate }}

+
    + @for (step of recipe.steps; track step.id) { +
  1. {{ step.description }}
  2. + } +
+
+
+ } + +
+
+
+

{{ 'pdf.shoppingList' | translate }}

+

+ {{ generatedOnLabel() }} +

+
+ +
    + @for (item of shoppingList(); track itemKey(item)) { +
  • + {{ item.name }} + — {{ quantityLabel(item) }} + @if (breakdownLabel(item); as breakdown) { + ({{ breakdown }}) + } +
  • + } +
+ +
+

+ {{ 'pdf.recipesInExport' | translate }} +

+
    + @for (r of recipes(); track r.id) { +
  1. + {{ r.name }} — {{ categoryLabel(r.mealCategory) }} + @if (r.calories != null) { + · {{ r.calories }} kcal + } +
  2. + } +
+
+
+
+
+ } + `, +}) +export class PdfGeneratorComponent implements OnDestroy { + private readonly recipeService = inject(RecipeService); + private readonly translate = inject(TranslateService); + private renderCancelled = false; + + @Input() selectedIds: number[] = []; + @Input() disabled = false; + + readonly phase = signal('idle'); + readonly recipes = signal([]); + readonly error = signal(null); + readonly pageStyle = PAGE_STYLE; + readonly formatIngredientAmount = formatIngredientAmount; + + readonly shoppingList = signal([]); + private generatedOn = ''; + + constructor() { + effect(() => { + if (this.phase() === 'rendering' && this.recipes().length > 0) { + void this.runPdfGeneration(); + } + }); + } + + ngOnDestroy(): void { + this.renderCancelled = true; + } + + categoryLabel(category: MealCategory): string { + return this.translate.instant(MEAL_CATEGORY_I18N_KEYS[category]); + } + + macros(recipe: RecipeDetail) { + return [ + { label: this.translate.instant('recipes.calories'), value: recipe.calories, suffix: 'kcal' }, + { label: this.translate.instant('recipes.protein'), value: recipe.protein, suffix: 'g' }, + { label: this.translate.instant('recipes.fat'), value: recipe.fat, suffix: 'g' }, + { label: this.translate.instant('recipes.carbs'), value: recipe.carbs, suffix: 'g' }, + ]; + } + + quantityLabel(item: ShoppingItem): string { + return formatQuantity(item, this.translate.instant('pdf.toTaste')); + } + + breakdownLabel(item: ShoppingItem): string | null { + return formatBreakdown(item); + } + + itemKey(item: ShoppingItem): string { + return `${item.name}-${item.unit ?? 'g'}-${item.quantified}`; + } + + generatedOnLabel(): string { + const count = this.recipes().length; + const lang = this.translate.getCurrentLang() ?? this.translate.getFallbackLang() ?? 'en'; + let suffix: string; + if (lang.startsWith('pl')) { + const mod10 = count % 10; + const mod100 = count % 100; + if (count === 1) suffix = 'one'; + else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) suffix = 'few'; + else suffix = 'many'; + } else { + suffix = count === 1 ? 'one' : 'other'; + } + return this.translate.instant(`pdf.generatedOn_${suffix}`, { + date: this.generatedOn, + count, + }); + } + + handleGenerate(): void { + if (this.selectedIds.length === 0 || this.phase() !== 'idle') return; + this.error.set(null); + this.phase.set('loading'); + + forkJoin(this.selectedIds.map((id) => this.recipeService.getRecipeById(id))).subscribe({ + next: (details) => { + this.generatedOn = new Date().toLocaleDateString( + this.translate.getCurrentLang() ?? 'en', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + this.shoppingList.set(buildShoppingList(details)); + this.recipes.set(details); + this.phase.set('rendering'); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.pdfLoadFailed'))); + this.phase.set('idle'); + }, + }); + } + + private async runPdfGeneration(): Promise { + this.renderCancelled = false; + await document.fonts?.ready?.catch(() => undefined); + await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); + if (this.renderCancelled) return; + + try { + await generatePdf(this.translate, 'dailymeals-plan.pdf'); + } catch (err) { + this.error.set(extractApiError(err, this.translate.instant('errors.pdfGenerateFailed'))); + } finally { + if (!this.renderCancelled) { + this.phase.set('idle'); + this.recipes.set([]); + this.shoppingList.set([]); + } + } + } +} diff --git a/meal-plan-frontend-angular/src/app/features/recipe-detail/recipe-detail-page.component.ts b/meal-plan-frontend-angular/src/app/features/recipe-detail/recipe-detail-page.component.ts new file mode 100644 index 0000000..9eb34f0 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/recipe-detail/recipe-detail-page.component.ts @@ -0,0 +1,84 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { RecipeService } from '../../core/services/recipe.service'; +import { RecipeDetail } from '../../core/models/recipe.models'; +import { RecipeDetailViewComponent } from '../recipes/recipe-detail-view.component'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +@Component({ + selector: 'app-recipe-detail-page', + standalone: true, + imports: [TranslatePipe, RecipeDetailViewComponent, ErrorStateComponent, SkeletonComponent], + template: ` +
+ + + @if (loading()) { +
+ + +
+ @for (i of [0, 1, 2, 3]; track i) { + + } +
+ +
+ } + + @if (error()) { + + } + + @if (!loading() && !error() && recipe()) { + + } +
+ `, +}) +export class RecipeDetailPageComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly location = inject(Location); + private readonly recipeService = inject(RecipeService); + private readonly translate = inject(TranslateService); + + readonly loading = signal(true); + readonly error = signal(null); + readonly recipe = signal(null); + + ngOnInit(): void { + this.load(); + } + + load(): void { + const id = Number(this.route.snapshot.paramMap.get('id')); + if (!Number.isFinite(id)) { + this.error.set(this.translate.instant('errors.loadRecipeFailed')); + this.loading.set(false); + return; + } + + this.loading.set(true); + this.error.set(null); + this.recipeService.getRecipeById(id).subscribe({ + next: (data) => { + this.recipe.set(data); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadRecipeFailed'))); + this.loading.set(false); + }, + }); + } + + goBack(): void { + this.location.back(); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/recipe-generator/recipe-generator.component.ts b/meal-plan-frontend-angular/src/app/features/recipe-generator/recipe-generator.component.ts new file mode 100644 index 0000000..2e5252d --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/recipe-generator/recipe-generator.component.ts @@ -0,0 +1,371 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { switchMap } from 'rxjs'; +import { RecipeGeneratorService } from '../../core/services/recipe-generator.service'; +import { + CommitRecipeResult, + GeneratedRecipeDraft, + RecipeGeneratorConstraints, +} from '../../core/models/generator.models'; +import { MEAL_CATEGORY_I18N_KEYS, MealCategory } from '../../core/models/recipe.models'; +import { extractApiError } from '../../core/utils/api-error.util'; + +type Step = 'constraints' | 'review' | 'done'; + +const MEAL_CATEGORIES = [ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, +]; + +@Component({ + selector: 'app-recipe-generator', + standalone: true, + imports: [FormsModule, RouterLink, TranslatePipe], + template: ` +
+
+

{{ 'generators.recipe.title' | translate }}

+

{{ 'generators.recipe.subtitle' | translate }}

+
+ + @if (error()) { + + } + + @if (step() === 'constraints') { +
+

{{ 'generators.recipe.constraintsTitle' | translate }}

+ + + + + + + +
+ } + + @if (step() === 'review' && drafts().length > 0) { +
+
+

+ {{ 'generators.recipe.reviewHint' | translate: { count: drafts().length } }} +

+ +
+ + @for (draft of drafts(); track draft.draftId) { +
+
+ + +
+ + @if (draft.unlinkedIngredientCount > 0) { +

+ {{ 'manage.unlinkedIngredientsWarning' | translate: { count: draft.unlinkedIngredientCount } }} +

+ } + +

{{ 'recipes.ingredients' | translate }}

+
    + @for (ing of draft.ingredients; track $index) { +
  • + {{ ing.name }} — {{ ing.amountGrams ?? '?' }}g {{ ing.unit ?? '' }} +
  • + } +
+

{{ 'manage.preparationSteps' | translate }}

+
    + @for (s of draft.steps; track s.stepNumber) { +
  1. {{ s.description }}
  2. + } +
+ +
+ + + +
+
+ } + +
+ + +
+
+ } + + @if (step() === 'done' && savedRecipes().length > 0) { +
+

{{ 'generators.recipe.doneMultiple' | translate: { count: savedRecipes().length } }}

+ + +
+ } +
+ `, +}) +export class RecipeGeneratorComponent { + private readonly recipeGenerator = inject(RecipeGeneratorService); + private readonly translate = inject(TranslateService); + + readonly mealCategories = MEAL_CATEGORIES; + readonly step = signal('constraints'); + readonly sessionId = signal(null); + readonly drafts = signal([]); + readonly selectedIds = signal>(new Set()); + readonly savedRecipes = signal([]); + readonly pending = signal(false); + readonly error = signal(null); + + recipeCount = 3; + constraints: RecipeGeneratorConstraints = { + prompt: '', + mealCategory: MealCategory.Breakfast, + targetCalories: null, + calorieToleranceKcal: 10, + maxPrepTimeMinutes: 30, + cuisineStyle: '', + dietStyle: '', + exclusions: '', + }; + + isWithinCalorieTarget(calories?: number | null): boolean { + if (!this.constraints.targetCalories || calories == null) return true; + const tolerance = this.constraints.calorieToleranceKcal ?? 10; + return Math.abs(calories - this.constraints.targetCalories) <= tolerance; + } + + readonly allSelected = computed(() => { + const list = this.drafts(); + const selected = this.selectedIds(); + return list.length > 0 && list.every((d) => selected.has(d.draftId)); + }); + + categoryLabel(cat: MealCategory): string { + return MEAL_CATEGORY_I18N_KEYS[cat]; + } + + private syncDrafts(next: GeneratedRecipeDraft[]): void { + this.drafts.set(next); + this.selectedIds.set(new Set(next.map((d) => d.draftId))); + } + + runGenerate(): void { + this.error.set(null); + this.pending.set(true); + const count = Math.min(8, Math.max(1, this.recipeCount || 3)); + const existingId = this.sessionId(); + const request$ = existingId + ? this.recipeGenerator.generateDrafts(existingId, count) + : this.recipeGenerator + .createSession(this.constraints) + .pipe( + switchMap((created) => { + this.sessionId.set(created.id); + return this.recipeGenerator.generateDrafts(created.id, count); + }), + ); + + request$.subscribe({ + next: (session) => { + if (!this.sessionId()) this.sessionId.set(session.id); + this.syncDrafts(session.drafts ?? []); + this.step.set('review'); + this.pending.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.recipe.error'))); + this.pending.set(false); + }, + }); + } + + regen(draftId: string, mode: 'ingredients' | 'steps' | 'full'): void { + const id = this.sessionId(); + if (!id) return; + this.pending.set(true); + this.error.set(null); + this.recipeGenerator.regeneratePart(id, draftId, mode, this.constraints.prompt).subscribe({ + next: (session) => { + this.syncDrafts(session.drafts ?? []); + this.pending.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.recipe.error'))); + this.pending.set(false); + }, + }); + } + + removeDraft(draftId: string): void { + const id = this.sessionId(); + if (!id) return; + this.pending.set(true); + this.error.set(null); + this.recipeGenerator.removeDraft(id, draftId).subscribe({ + next: (session) => { + const next = session.drafts ?? []; + this.syncDrafts(next); + if (next.length === 0) this.step.set('constraints'); + this.pending.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.recipe.error'))); + this.pending.set(false); + }, + }); + } + + saveSelected(): void { + const id = this.sessionId(); + const ids = [...this.selectedIds()]; + if (!id || ids.length === 0) return; + this.pending.set(true); + this.error.set(null); + this.recipeGenerator.commitRecipes(id, ids).subscribe({ + next: (result) => { + this.savedRecipes.set(result.saved); + const remaining = this.drafts().filter((d) => !this.selectedIds().has(d.draftId)); + if (remaining.length === 0) { + this.step.set('done'); + } else { + this.syncDrafts(remaining); + } + this.pending.set(false); + }, + error: (e) => { + this.error.set(extractApiError(e, this.translate.instant('generators.recipe.error'))); + this.pending.set(false); + }, + }); + } + + toggleSelected(draftId: string): void { + this.selectedIds.update((prev) => { + const next = new Set(prev); + if (next.has(draftId)) next.delete(draftId); + else next.add(draftId); + return next; + }); + } + + toggleAll(): void { + if (this.allSelected()) { + this.selectedIds.set(new Set()); + } else { + this.selectedIds.set(new Set(this.drafts().map((d) => d.draftId))); + } + } + + reset(): void { + this.step.set('constraints'); + this.sessionId.set(null); + this.drafts.set([]); + this.selectedIds.set(new Set()); + this.savedRecipes.set([]); + this.error.set(null); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/recipes/recipe-card.component.ts b/meal-plan-frontend-angular/src/app/features/recipes/recipe-card.component.ts new file mode 100644 index 0000000..875cd12 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/recipes/recipe-card.component.ts @@ -0,0 +1,67 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { RecipeListItem } from '../../core/models/recipe.models'; +import { CategoryBadgeComponent } from '../../shared/components/category-badge.component'; + +@Component({ + selector: 'app-recipe-card', + standalone: true, + imports: [RouterLink, TranslatePipe, CategoryBadgeComponent], + template: ` + + `, +}) +export class RecipeCardComponent { + @Input({ required: true }) recipe!: RecipeListItem; + @Input() selectable = false; + @Input() selected = false; + @Input() showCategory = false; + @Output() toggleSelected = new EventEmitter(); + + onToggle(): void { + this.toggleSelected.emit(this.recipe.id); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/recipes/recipe-detail-view.component.ts b/meal-plan-frontend-angular/src/app/features/recipes/recipe-detail-view.component.ts new file mode 100644 index 0000000..98d1b27 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/recipes/recipe-detail-view.component.ts @@ -0,0 +1,154 @@ +import { Component, Input, computed, signal } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { RecipeDetail } from '../../core/models/recipe.models'; +import { CategoryBadgeComponent } from '../../shared/components/category-badge.component'; +import { nextSort, sortBy, sortIndicator, type SortDirection } from '../../core/utils/sort.util'; + +type IngredientSortColumn = 'name' | 'amount'; + +@Component({ + selector: 'app-recipe-detail-view', + standalone: true, + imports: [TranslatePipe, CategoryBadgeComponent], + template: ` +
+
+ +

{{ recipe.name }}

+ @if (recipe.prepTimeMinutes != null) { +

+ + {{ 'recipes.prepTime' | translate: { count: recipe.prepTimeMinutes } }} +

+ } +
+ +
+
+

+ {{ recipe.calories ?? '—' }} + @if (recipe.calories != null) { + kcal + } +

+

+ {{ 'recipes.calories' | translate }} +

+
+
+

+ {{ recipe.protein ?? '—' }} + @if (recipe.protein != null) { + g + } +

+

+ {{ 'recipes.protein' | translate }} +

+
+
+

+ {{ recipe.fat ?? '—' }} + @if (recipe.fat != null) { + g + } +

+

+ {{ 'recipes.fat' | translate }} +

+
+
+

+ {{ recipe.carbs ?? '—' }} + @if (recipe.carbs != null) { + g + } +

+

+ {{ 'recipes.carbs' | translate }} +

+
+
+ +
+
+

{{ 'recipes.ingredients' | translate }}

+ @if (recipe.ingredients.length === 0) { +

{{ 'recipes.noIngredients' | translate }}

+ } @else { +
+
+ + +
+
    + @for (ing of sortedIngredients(); track ing.id) { +
  • + {{ ing.name }} + + {{ formatAmount(ing.amountGrams, ing.unit) }} + +
  • + } +
+
+ } +
+ +
+

{{ 'recipes.preparation' | translate }}

+ @if (recipe.steps.length === 0) { +

{{ 'recipes.noSteps' | translate }}

+ } @else { +
    + @for (step of recipe.steps; track step.id) { +
  1. + + {{ step.stepNumber }} + +

    + {{ step.description }} +

    +
  2. + } +
+ } +
+
+
+ `, +}) +export class RecipeDetailViewComponent { + @Input({ required: true }) recipe!: RecipeDetail; + + readonly ingredientSort = signal<{ column: IngredientSortColumn; direction: SortDirection }>({ + column: 'name', + direction: 'asc', + }); + + readonly sortedIngredients = computed(() => { + const { column, direction } = this.ingredientSort(); + return sortBy(this.recipe.ingredients, direction, (ing) => + column === 'name' ? ing.name : ing.amountGrams, + ); + }); + + formatAmount(amount: number | null, unit: string | null): string { + if (amount == null && !unit) return ''; + if (amount == null) return unit ?? ''; + const display = Number.isInteger(amount) ? amount.toString() : amount.toString(); + return unit ? `${display} ${unit}` : `${display} g`; + } + + toggleIngredientSort(column: IngredientSortColumn): void { + this.ingredientSort.update((current) => nextSort(current, column)); + } + + ingredientIndicator(column: IngredientSortColumn): string { + return sortIndicator(this.ingredientSort(), column); + } +} diff --git a/meal-plan-frontend-angular/src/app/features/recipes/recipe-filters.component.ts b/meal-plan-frontend-angular/src/app/features/recipes/recipe-filters.component.ts new file mode 100644 index 0000000..de4cb40 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/recipes/recipe-filters.component.ts @@ -0,0 +1,103 @@ +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { MealCategory, MEAL_CATEGORY_I18N_KEYS } from '../../core/models/recipe.models'; + +export type CategoryFilter = MealCategory | 'all'; + +@Component({ + selector: 'app-recipe-filters', + standalone: true, + imports: [FormsModule, TranslatePipe], + template: ` +
+
+
+ @for (pill of pills; track pill.value) { + + } +
+ +
+ @if (searchByIngredient) { + + } @else { + + } + +
+
+ + +
+ `, +}) +export class RecipeFiltersComponent { + private readonly translate = inject(TranslateService); + + @Input({ required: true }) active!: CategoryFilter; + @Output() onChange = new EventEmitter(); + @Input({ required: true }) search!: string; + @Output() onSearchChange = new EventEmitter(); + @Input() searchByIngredient = false; + @Output() onSearchByIngredientChange = new EventEmitter(); + + get pills(): Array<{ value: CategoryFilter; label: string }> { + return [ + { value: 'all', label: this.translate.instant('common.all') }, + ...[ + MealCategory.Breakfast, + MealCategory.SecondBreakfast, + MealCategory.Lunch, + MealCategory.Dinner, + ].map((cat) => ({ + value: cat as CategoryFilter, + label: this.translate.instant(MEAL_CATEGORY_I18N_KEYS[cat]), + })), + ]; + } +} diff --git a/meal-plan-frontend-angular/src/app/features/shopping-list/shopping-list.component.ts b/meal-plan-frontend-angular/src/app/features/shopping-list/shopping-list.component.ts new file mode 100644 index 0000000..37f9890 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/features/shopping-list/shopping-list.component.ts @@ -0,0 +1,165 @@ +import { DecimalPipe } from '@angular/common'; +import { Component, OnInit, computed, inject, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { MealPlanService } from '../../core/services/meal-plan.service'; +import { ShoppingListItem } from '../../core/models/meal-plan.models'; +import { + addDaysIso, + endOfWeekIso, + startOfWeekIso, + todayIso, +} from '../../core/models/diet.models'; +import { ErrorStateComponent } from '../../shared/components/error-state.component'; +import { SkeletonComponent } from '../../shared/components/skeleton.component'; +import { extractApiError } from '../../core/utils/api-error.util'; + +const CHECKLIST_KEY = 'dm_shopping_checklist'; + +@Component({ + selector: 'app-shopping-list', + standalone: true, + imports: [DecimalPipe, FormsModule, TranslatePipe, ErrorStateComponent, SkeletonComponent], + template: ` +
+
+

{{ 'shoppingList.title' | translate }}

+

{{ 'shoppingList.subtitle' | translate }}

+
+ +
+ + + + +
+ + @if (loading()) { + + } @else if (error()) { + + } @else if (items().length === 0 && loaded()) { +

{{ 'shoppingList.empty' | translate }}

+ } @else { +
    + @for (item of items(); track itemKey(item)) { +
  • + +
    +

    {{ item.name }}

    +

    + @if (item.totalGrams != null) { + {{ item.totalGrams | number: '1.0-1' }}g + } @else if (item.totalAmount != null) { + {{ item.totalAmount | number: '1.0-1' }} {{ item.unit ?? '' }} + } +

    + @if (item.breakdown.length > 0) { +
      + @for (line of item.breakdown; track line) { +
    • {{ line }}
    • + } +
    + } +
    +
  • + } +
+

{{ 'shoppingList.checklistHint' | translate }}

+ } +
+ `, +}) +export class ShoppingListComponent implements OnInit { + private readonly mealPlan = inject(MealPlanService); + private readonly translate = inject(TranslateService); + + fromDate = startOfWeekIso(todayIso()); + toDate = endOfWeekIso(todayIso()); + items = signal([]); + checkedKeys = signal>(new Set()); + loading = signal(false); + loaded = signal(false); + error = signal(null); + + readonly rangeKey = computed(() => `${this.fromDate}_${this.toDate}`); + + ngOnInit(): void { + this.restoreChecks(); + this.loadList(); + } + + itemKey(item: ShoppingListItem): string { + return `${item.name}|${item.unit ?? ''}`; + } + + isChecked(item: ShoppingListItem): boolean { + return this.checkedKeys().has(this.itemKey(item)); + } + + toggleCheck(item: ShoppingListItem): void { + const key = this.itemKey(item); + this.checkedKeys.update((set) => { + const next = new Set(set); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + this.persistChecks(); + } + + clearChecks(): void { + this.checkedKeys.set(new Set()); + this.persistChecks(); + } + + loadList(): void { + this.loading.set(true); + this.error.set(null); + this.restoreChecks(); + this.mealPlan.getShoppingList(this.fromDate, this.toDate).subscribe({ + next: (list) => { + this.items.set(list); + this.loaded.set(true); + this.loading.set(false); + }, + error: (err) => { + this.error.set(extractApiError(err, this.translate.instant('errors.loadShoppingListFailed'))); + this.loading.set(false); + }, + }); + } + + private storageKey(): string { + return `${CHECKLIST_KEY}_${this.rangeKey()}`; + } + + private restoreChecks(): void { + try { + const raw = localStorage.getItem(this.storageKey()); + if (!raw) { + this.checkedKeys.set(new Set()); + return; + } + const parsed = JSON.parse(raw) as string[]; + this.checkedKeys.set(new Set(parsed)); + } catch { + this.checkedKeys.set(new Set()); + } + } + + private persistChecks(): void { + localStorage.setItem(this.storageKey(), JSON.stringify([...this.checkedKeys()])); + } +} diff --git a/meal-plan-frontend-angular/src/app/layout/app-layout.component.ts b/meal-plan-frontend-angular/src/app/layout/app-layout.component.ts new file mode 100644 index 0000000..2962c29 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/app-layout.component.ts @@ -0,0 +1,76 @@ +import { Component, OnInit, computed, inject, signal } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { LayoutService } from '../core/services/layout.service'; +import { ReminderNotificationService } from '../core/services/reminder-notification.service'; +import { HorizontalNavComponent } from './horizontal-nav.component'; +import { NavbarComponent } from './navbar.component'; +import { SidebarComponent, SidebarVariant } from './sidebar.component'; + +@Component({ + selector: 'app-layout', + standalone: true, + imports: [RouterOutlet, NavbarComponent, SidebarComponent, HorizontalNavComponent], + template: ` + @if (layoutService.layout() === 'topnav') { +
+ + +
+ +
+
+ } @else { +
+ +
+ +
+ +
+
+
+ } + `, +}) +export class AppLayoutComponent implements OnInit { + readonly layoutService = inject(LayoutService); + private readonly reminders = inject(ReminderNotificationService); + readonly sidebarOpen = signal(false); + + ngOnInit(): void { + this.reminders.start(); + } + + readonly sidebarVariant = computed((): SidebarVariant => { + const id = this.layoutService.layout(); + if (id === 'compact') return 'compact'; + if (id === 'wide') return 'wide'; + return 'default'; + }); + + readonly mainClass = computed(() => { + const base = 'mx-auto w-full flex-1 px-4 py-6 sm:px-6 sm:py-8'; + switch (this.layoutService.layout()) { + case 'topnav': + return `${base} max-w-7xl`; + case 'wide': + return `${base} max-w-6xl`; + case 'compact': + return `${base} max-w-6xl`; + default: + return `${base} max-w-6xl`; + } + }); + + toggleSidebar(): void { + this.sidebarOpen.update((v) => !v); + } + + closeSidebar(): void { + this.sidebarOpen.set(false); + } +} diff --git a/meal-plan-frontend-angular/src/app/layout/horizontal-nav.component.ts b/meal-plan-frontend-angular/src/app/layout/horizontal-nav.component.ts new file mode 100644 index 0000000..52cdcc6 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/horizontal-nav.component.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { NAV_ITEMS } from './nav-items'; +import { NavIconComponent } from './nav-icon.component'; + +@Component({ + selector: 'app-horizontal-nav', + standalone: true, + imports: [RouterLink, RouterLinkActive, TranslatePipe, NavIconComponent], + template: ` + + `, +}) +export class HorizontalNavComponent { + @Output() navigate = new EventEmitter(); + readonly navItems = NAV_ITEMS; +} diff --git a/meal-plan-frontend-angular/src/app/layout/nav-icon.component.ts b/meal-plan-frontend-angular/src/app/layout/nav-icon.component.ts new file mode 100644 index 0000000..0824ae4 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/nav-icon.component.ts @@ -0,0 +1,86 @@ +import { Component, Input, inject } from '@angular/core'; +import type { NavIcon } from './nav-items'; +import { NAV_ICON_EMOJI } from '../core/theme/appearance.config'; +import { AppearanceService } from '../core/services/appearance.service'; + +@Component({ + selector: 'app-nav-icon', + standalone: true, + template: ` + @if (appearance.useEmojiIcons()) { + + } @else { + @switch (icon) { + @case ('dashboard') { + + } + @case ('coffee') { + + } + @case ('croissant') { + + } + @case ('soup') { + + } + @case ('moon') { + + } + @case ('list') { + + } + @case ('book') { + + } + @case ('leaf') { + + } + @case ('diet') { + + } + @case ('calendar') { + + } + @case ('cart') { + + } + } + } + `, +}) +export class NavIconComponent { + readonly appearance = inject(AppearanceService); + @Input({ required: true }) icon!: NavIcon; + + emoji(): string { + return NAV_ICON_EMOJI[this.icon] ?? '•'; + } +} diff --git a/meal-plan-frontend-angular/src/app/layout/nav-items.ts b/meal-plan-frontend-angular/src/app/layout/nav-items.ts new file mode 100644 index 0000000..1616015 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/nav-items.ts @@ -0,0 +1,23 @@ +export type NavIcon = 'dashboard' | 'coffee' | 'croissant' | 'soup' | 'moon' | 'list' | 'book' | 'leaf' | 'diet' | 'calendar' | 'cart'; + +export interface NavItem { + to: string; + labelKey: string; + icon: NavIcon; +} + +export const NAV_ITEMS: NavItem[] = [ + { to: '/', labelKey: 'nav.dashboard', icon: 'dashboard' }, + { to: '/breakfast', labelKey: 'nav.breakfast', icon: 'coffee' }, + { to: '/second-breakfast', labelKey: 'nav.secondBreakfast', icon: 'croissant' }, + { to: '/lunch', labelKey: 'nav.lunch', icon: 'soup' }, + { to: '/dinner', labelKey: 'nav.dinner', icon: 'moon' }, + { to: '/all-meals', labelKey: 'nav.allMeals', icon: 'list' }, + { to: '/meal-planner', labelKey: 'nav.mealPlanner', icon: 'calendar' }, + { to: '/diet-generator', labelKey: 'nav.dietGenerator', icon: 'diet' }, + { to: '/recipe-generator', labelKey: 'nav.recipeGenerator', icon: 'book' }, + { to: '/shopping-list', labelKey: 'nav.shoppingList', icon: 'cart' }, + { to: '/manage-meals', labelKey: 'nav.manageMeals', icon: 'book' }, + { to: '/ingredients', labelKey: 'nav.ingredientCatalog', icon: 'leaf' }, + { to: '/diet', labelKey: 'nav.dietTracker', icon: 'diet' }, +]; diff --git a/meal-plan-frontend-angular/src/app/layout/navbar.component.ts b/meal-plan-frontend-angular/src/app/layout/navbar.component.ts new file mode 100644 index 0000000..d9295aa --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/navbar.component.ts @@ -0,0 +1,101 @@ +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { AuthService } from '../core/services/auth.service'; +import { ThemeService } from '../core/services/theme.service'; +import { AppearanceSwitcherComponent } from '../shared/components/appearance-switcher.component'; +import { LanguageSwitcherComponent } from '../shared/components/language-switcher.component'; +import { LayoutSwitcherComponent } from '../shared/components/layout-switcher.component'; + +@Component({ + selector: 'app-navbar', + standalone: true, + imports: [RouterLink, TranslatePipe, AppearanceSwitcherComponent, LanguageSwitcherComponent, LayoutSwitcherComponent], + template: ` +
+
+
+ @if (showMenuToggle) { + + } + + + + + {{ 'app.name' | translate }} + +
+ +
+ + + + + + + + + +
+
+
+ `, +}) +export class NavbarComponent { + readonly auth = inject(AuthService); + readonly theme = inject(ThemeService); + private readonly router = inject(Router); + + @Input() showMenuToggle = true; + @Output() toggleSidebar = new EventEmitter(); + + async logout(): Promise { + await this.auth.logout(); + void this.router.navigateByUrl('/login'); + } +} diff --git a/meal-plan-frontend-angular/src/app/layout/sidebar.component.ts b/meal-plan-frontend-angular/src/app/layout/sidebar.component.ts new file mode 100644 index 0000000..54cad3a --- /dev/null +++ b/meal-plan-frontend-angular/src/app/layout/sidebar.component.ts @@ -0,0 +1,92 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import type { LayoutId } from '../core/services/layout.service'; +import { NAV_ITEMS } from './nav-items'; +import { NavIconComponent } from './nav-icon.component'; + +export type SidebarVariant = 'default' | 'compact' | 'wide'; + +@Component({ + selector: 'app-sidebar', + standalone: true, + imports: [RouterLink, RouterLinkActive, TranslatePipe, NavIconComponent], + template: ` + @if (open) { + + } + + `, +}) +export class SidebarComponent { + @Input() open = false; + @Input() variant: SidebarVariant = 'default'; + @Output() navigate = new EventEmitter(); + + readonly navItems = NAV_ITEMS; + + asideClass(): string { + const base = + 'border-slate-200 bg-white px-3 py-6 dark:border-slate-800 dark:bg-slate-900'; + switch (this.variant) { + case 'compact': + return `w-16 ${base}`; + case 'wide': + return `w-72 ${base} bg-gradient-to-b from-white to-brand-50/40 dark:from-slate-900 dark:to-brand-950/20`; + default: + return `w-64 ${base}`; + } + } + + navClass(): string { + return this.variant === 'compact' + ? 'mt-16 flex flex-col items-center gap-2 lg:mt-0' + : 'mt-16 flex flex-col gap-1 lg:mt-0'; + } + + linkClass(): string { + const base = + 'flex items-center rounded-xl text-sm font-medium text-slate-600 transition hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800 [&.bg-brand-600]:text-white'; + if (this.variant === 'compact') { + return `${base} justify-center p-2.5`; + } + if (this.variant === 'wide') { + return `${base} gap-3 px-4 py-3 text-[0.95rem]`; + } + return `${base} gap-3 px-3.5 py-2.5`; + } +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/appearance-switcher.component.ts b/meal-plan-frontend-angular/src/app/shared/components/appearance-switcher.component.ts new file mode 100644 index 0000000..d6ff95a --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/appearance-switcher.component.ts @@ -0,0 +1,39 @@ +import { Component, inject } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { AppearanceId } from '../../core/theme/appearance.config'; +import { AppearanceService } from '../../core/services/appearance.service'; + +@Component({ + selector: 'app-appearance-switcher', + standalone: true, + imports: [TranslatePipe], + template: ` + +
+ + +
+ `, +}) +export class AppearanceSwitcherComponent { + readonly appearance = inject(AppearanceService); + + onChange(event: Event): void { + const value = (event.target as HTMLSelectElement).value as AppearanceId; + this.appearance.setProposal(value); + } +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/category-badge.component.ts b/meal-plan-frontend-angular/src/app/shared/components/category-badge.component.ts new file mode 100644 index 0000000..130c4c5 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/category-badge.component.ts @@ -0,0 +1,35 @@ +import { Component, Input } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { MealCategory, MEAL_CATEGORY_I18N_KEYS } from '../../core/models/recipe.models'; + +const STYLES: Record = { + [MealCategory.Breakfast]: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300', + [MealCategory.SecondBreakfast]: 'bg-sky-100 text-sky-800 dark:bg-sky-900/40 dark:text-sky-300', + [MealCategory.Lunch]: 'bg-brand-100 text-brand-800 dark:bg-brand-900/40 dark:text-brand-300', + [MealCategory.Dinner]: 'bg-violet-100 text-violet-800 dark:bg-violet-900/40 dark:text-violet-300', +}; + +@Component({ + selector: 'app-category-badge', + standalone: true, + imports: [TranslatePipe], + template: ` + + {{ labelKey | translate }} + + `, +}) +export class CategoryBadgeComponent { + @Input({ required: true }) category!: MealCategory; + + get labelKey(): string { + return MEAL_CATEGORY_I18N_KEYS[this.category] ?? 'categories.unknown'; + } + + get styles(): string { + return STYLES[this.category] ?? ''; + } +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/empty-state.component.ts b/meal-plan-frontend-angular/src/app/shared/components/empty-state.component.ts new file mode 100644 index 0000000..7972ac6 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/empty-state.component.ts @@ -0,0 +1,26 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-empty-state', + standalone: true, + template: ` +
+
+ +
+

{{ title }}

+ @if (description) { +

{{ description }}

+ } +
+ `, +}) +export class EmptyStateComponent { + @Input({ required: true }) title!: string; + @Input() description?: string; +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/error-state.component.ts b/meal-plan-frontend-angular/src/app/shared/components/error-state.component.ts new file mode 100644 index 0000000..c36ac6b --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/error-state.component.ts @@ -0,0 +1,31 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; + +@Component({ + selector: 'app-error-state', + standalone: true, + imports: [TranslatePipe], + template: ` + + `, +}) +export class ErrorStateComponent { + @Input({ required: true }) message!: string; + @Input() showRetry = false; + @Output() retry = new EventEmitter(); +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/language-switcher.component.ts b/meal-plan-frontend-angular/src/app/shared/components/language-switcher.component.ts new file mode 100644 index 0000000..b9e7a74 --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/language-switcher.component.ts @@ -0,0 +1,41 @@ +import { Component, inject } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { LanguageService } from '../../core/services/language.service'; + +@Component({ + selector: 'app-language-switcher', + standalone: true, + imports: [TranslatePipe], + template: ` +
+ + +
+ `, +}) +export class LanguageSwitcherComponent { + readonly language = inject(LanguageService); + className = ''; + + onChange(event: Event): void { + const value = (event.target as HTMLSelectElement).value as 'en' | 'pl'; + this.language.setLanguage(value); + } +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/layout-switcher.component.ts b/meal-plan-frontend-angular/src/app/shared/components/layout-switcher.component.ts new file mode 100644 index 0000000..db4ce5f --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/layout-switcher.component.ts @@ -0,0 +1,38 @@ +import { Component, inject } from '@angular/core'; +import { TranslatePipe } from '@ngx-translate/core'; +import { LayoutId, LayoutService } from '../../core/services/layout.service'; + +@Component({ + selector: 'app-layout-switcher', + standalone: true, + imports: [TranslatePipe], + template: ` + + + `, +}) +export class LayoutSwitcherComponent { + readonly layout = inject(LayoutService); + + readonly options: Array<{ id: LayoutId; labelKey: string }> = [ + { id: 'sidebar', labelKey: 'layout.sidebar' }, + { id: 'topnav', labelKey: 'layout.topnav' }, + { id: 'compact', labelKey: 'layout.compact' }, + { id: 'wide', labelKey: 'layout.wide' }, + ]; + + onChange(event: Event): void { + const value = (event.target as HTMLSelectElement).value as LayoutId; + this.layout.setLayout(value); + } +} diff --git a/meal-plan-frontend-angular/src/app/shared/components/skeleton.component.ts b/meal-plan-frontend-angular/src/app/shared/components/skeleton.component.ts new file mode 100644 index 0000000..21e6cbe --- /dev/null +++ b/meal-plan-frontend-angular/src/app/shared/components/skeleton.component.ts @@ -0,0 +1,43 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-skeleton', + standalone: true, + template: ` + + `, +}) +export class SkeletonComponent { + @Input() className = ''; +} + +@Component({ + selector: 'app-grid-skeleton', + standalone: true, + imports: [SkeletonComponent], + template: ` +
+ @for (item of items; track item) { +
+ + +
+ + +
+
+ } +
+ `, +}) +export class GridSkeletonComponent { + @Input() count = 6; + + get items(): number[] { + return Array.from({ length: this.count }, (_, i) => i); + } +} diff --git a/meal-plan-frontend-angular/src/index.html b/meal-plan-frontend-angular/src/index.html new file mode 100644 index 0000000..2b3a693 --- /dev/null +++ b/meal-plan-frontend-angular/src/index.html @@ -0,0 +1,16 @@ + + + + + DailyMeals + + + + + + + + + + + diff --git a/meal-plan-frontend-angular/src/main.ts b/meal-plan-frontend-angular/src/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/meal-plan-frontend-angular/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/meal-plan-frontend-angular/src/styles.css b/meal-plan-frontend-angular/src/styles.css new file mode 100644 index 0000000..144a82d --- /dev/null +++ b/meal-plan-frontend-angular/src/styles.css @@ -0,0 +1,132 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root, + [data-color-theme='forest'] { + --brand-50: #eefaf3; + --brand-100: #d6f1e0; + --brand-200: #b0e3c6; + --brand-300: #7dcda4; + --brand-400: #47b07e; + --brand-500: #22935f; + --brand-600: #16774c; + --brand-700: #125f3f; + --brand-800: #114c34; + --brand-900: #0e3e2c; + --surface-bg: #f8fafc; + --surface-bg-dark: #020617; + } + + [data-color-theme='ocean'] { + --brand-50: #eff6ff; + --brand-100: #dbeafe; + --brand-200: #bfdbfe; + --brand-300: #93c5fd; + --brand-400: #60a5fa; + --brand-500: #3b82f6; + --brand-600: #2563eb; + --brand-700: #1d4ed8; + --brand-800: #1e40af; + --brand-900: #1e3a8a; + --surface-bg: #f0f9ff; + --surface-bg-dark: #0c1222; + } + + [data-color-theme='sunset'] { + --brand-50: #fffbeb; + --brand-100: #fef3c7; + --brand-200: #fde68a; + --brand-300: #fcd34d; + --brand-400: #fbbf24; + --brand-500: #f59e0b; + --brand-600: #d97706; + --brand-700: #b45309; + --brand-800: #92400e; + --brand-900: #78350f; + --surface-bg: #fffbeb; + --surface-bg-dark: #1c1410; + } + + [data-color-theme='berry'] { + --brand-50: #f5f3ff; + --brand-100: #ede9fe; + --brand-200: #ddd6fe; + --brand-300: #c4b5fd; + --brand-400: #a78bfa; + --brand-500: #8b5cf6; + --brand-600: #7c3aed; + --brand-700: #6d28d9; + --brand-800: #5b21b6; + --brand-900: #4c1d95; + --surface-bg: #faf5ff; + --surface-bg-dark: #140f1f; + } + + html { + @apply antialiased; + } + + body { + background-color: var(--surface-bg); + @apply text-slate-900 dark:text-slate-100; + } + + .dark body { + background-color: var(--surface-bg-dark); + } + + :focus-visible { + @apply outline-none ring-2 ring-brand-500 ring-offset-2 ring-offset-white dark:ring-offset-slate-950; + } +} + +@layer components { + .card { + @apply rounded-2xl border border-slate-200 bg-white shadow-sm transition + dark:border-slate-800 dark:bg-slate-900; + } + + .btn { + @apply inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold + transition focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50; + } + + .btn-primary { + @apply btn bg-brand-600 text-white hover:bg-brand-700 active:bg-brand-800; + } + + .btn-secondary { + @apply btn border border-slate-300 bg-white text-slate-700 hover:bg-slate-100 + dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700; + } + + .input { + @apply w-full rounded-xl border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-900 + placeholder:text-slate-400 focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-300 + dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus:ring-brand-700; + } + + .select { + @apply input appearance-none bg-none pr-10; + min-height: 2.75rem; + height: 2.75rem; + } + + .sr-only { + @apply absolute -m-px h-px w-px overflow-hidden whitespace-nowrap border-0 p-0; + clip: rect(0, 0, 0, 0); + } +} + +#pdf-render-area { + position: fixed; + left: -10000px; + top: 0; + width: 794px; + background: #ffffff; + color: #0f172a; + z-index: -1; + pointer-events: none; +} diff --git a/meal-plan-frontend-angular/tailwind.config.js b/meal-plan-frontend-angular/tailwind.config.js new file mode 100644 index 0000000..a765efa --- /dev/null +++ b/meal-plan-frontend-angular/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./src/**/*.{html,ts}'], + darkMode: 'class', + theme: { + extend: { + colors: { + brand: { + 50: 'var(--brand-50)', + 100: 'var(--brand-100)', + 200: 'var(--brand-200)', + 300: 'var(--brand-300)', + 400: 'var(--brand-400)', + 500: 'var(--brand-500)', + 600: 'var(--brand-600)', + 700: 'var(--brand-700)', + 800: 'var(--brand-800)', + 900: 'var(--brand-900)', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], + }, + }, + }, + plugins: [], +}; diff --git a/meal-plan-frontend-angular/tsconfig.app.json b/meal-plan-frontend-angular/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/meal-plan-frontend-angular/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/meal-plan-frontend-angular/tsconfig.json b/meal-plan-frontend-angular/tsconfig.json new file mode 100644 index 0000000..5525117 --- /dev/null +++ b/meal-plan-frontend-angular/tsconfig.json @@ -0,0 +1,27 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/meal-plan-frontend-angular/tsconfig.spec.json b/meal-plan-frontend-angular/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/meal-plan-frontend-angular/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/meal-plan-frontend-mobile/.claude/settings.json b/meal-plan-frontend-mobile/.claude/settings.json new file mode 100644 index 0000000..176e6a5 --- /dev/null +++ b/meal-plan-frontend-mobile/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "expo@claude-plugins-official": true + } +} diff --git a/meal-plan-frontend-mobile/.gitignore b/meal-plan-frontend-mobile/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/meal-plan-frontend-mobile/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/meal-plan-frontend-mobile/.vscode/extensions.json b/meal-plan-frontend-mobile/.vscode/extensions.json new file mode 100644 index 0000000..b7ed837 --- /dev/null +++ b/meal-plan-frontend-mobile/.vscode/extensions.json @@ -0,0 +1 @@ +{ "recommendations": ["expo.vscode-expo-tools"] } diff --git a/meal-plan-frontend-mobile/.vscode/settings.json b/meal-plan-frontend-mobile/.vscode/settings.json new file mode 100644 index 0000000..e2798e4 --- /dev/null +++ b/meal-plan-frontend-mobile/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit", + "source.sortMembers": "explicit" + } +} diff --git a/meal-plan-frontend-mobile/AGENTS.md b/meal-plan-frontend-mobile/AGENTS.md new file mode 100644 index 0000000..a26b4bb --- /dev/null +++ b/meal-plan-frontend-mobile/AGENTS.md @@ -0,0 +1,3 @@ +# Expo HAS CHANGED + +Read the exact versioned docs at https://docs.expo.dev/versions/v56.0.0/ before writing any code. diff --git a/meal-plan-frontend-mobile/CLAUDE.md b/meal-plan-frontend-mobile/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/meal-plan-frontend-mobile/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/meal-plan-frontend-mobile/LICENSE b/meal-plan-frontend-mobile/LICENSE new file mode 100644 index 0000000..30b20e3 --- /dev/null +++ b/meal-plan-frontend-mobile/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/meal-plan-frontend-mobile/README.md b/meal-plan-frontend-mobile/README.md new file mode 100644 index 0000000..4dac1eb --- /dev/null +++ b/meal-plan-frontend-mobile/README.md @@ -0,0 +1,87 @@ +# DailyMeals Mobile + +Cross-platform **iOS** and **Android** client for the DailyMeals API, built with [Expo](https://expo.dev/) (~56) and [Expo Router](https://docs.expo.dev/router/introduction/). + +## Features + +- Sign in / register with JWT (tokens stored in SecureStore) +- Dashboard with meal categories +- Browse and search all recipes +- Recipe detail (ingredients + steps) +- Add recipes manually +- English / Polish (i18n) +- Four color themes (forest, ocean, sunset, berry) + +## Prerequisites + +- Node.js 20+ +- [Expo Go](https://expo.dev/go) on a physical device, or Xcode (iOS Simulator) / Android Studio (emulator) +- DailyMeals API running locally (default `http://localhost:5000`) + +## Setup + +```bash +cd meal-plan-frontend-mobile +npm install +``` + +Optional API override: + +```bash +export EXPO_PUBLIC_API_URL=http://192.168.1.10:5000/api +``` + +When unset, dev builds use: + +| Platform | Default API base | +|----------|------------------| +| iOS Simulator | `http://localhost:5000/api` | +| Android Emulator | `http://10.0.2.2:5000/api` | +| Physical device | Set `EXPO_PUBLIC_API_URL` to your machine's LAN IP | + +## Run + +```bash +npm start +``` + +Then press: + +- `i` — iOS Simulator +- `a` — Android Emulator +- Scan QR code — Expo Go on a phone (same network; set `EXPO_PUBLIC_API_URL`) + +Or: + +```bash +npm run ios +npm run android +``` + +## Project structure + +``` +app/ Expo Router screens + (auth)/ Login & register + (main)/(tabs)/ Dashboard, meals, manage, settings + (main)/recipe/[id] Recipe detail + (main)/category/[category] +src/ + api/ Fetch client + API modules + context/ Auth & appearance + i18n/ EN/PL translations + theme/ Color presets + components/ Shared UI +``` + +## Bundle identifiers + +- iOS: `com.dailymeals.mobile` +- Android: `com.dailymeals.mobile` + +Configured in `app.config.ts`. + +## Notes + +- Excel import is available on the web clients only; mobile supports manual recipe entry. +- PDF export is not included on mobile. diff --git a/meal-plan-frontend-mobile/app.config.ts b/meal-plan-frontend-mobile/app.config.ts new file mode 100644 index 0000000..0faa635 --- /dev/null +++ b/meal-plan-frontend-mobile/app.config.ts @@ -0,0 +1,41 @@ +import { ExpoConfig, ConfigContext } from 'expo/config'; + +export default ({ config }: ConfigContext): ExpoConfig => ({ + ...config, + name: 'DailyMeals', + slug: 'dailymeals-mobile', + version: '1.0.0', + orientation: 'portrait', + icon: './assets/images/icon.png', + scheme: 'dailymeals', + userInterfaceStyle: 'automatic', + ios: { + supportsTablet: true, + bundleIdentifier: 'com.dailymeals.mobile', + }, + android: { + adaptiveIcon: { + backgroundColor: '#16774c', + foregroundImage: './assets/images/android-icon-foreground.png', + backgroundImage: './assets/images/android-icon-background.png', + monochromeImage: './assets/images/android-icon-monochrome.png', + }, + package: 'com.dailymeals.mobile', + predictiveBackGestureEnabled: false, + }, + web: { + bundler: 'metro', + output: 'static', + favicon: './assets/images/favicon.png', + }, + plugins: ['expo-router', 'expo-splash-screen', 'expo-notifications', 'expo-sharing'], + experiments: { + typedRoutes: true, + }, + extra: { + apiUrl: process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:5000/api', + eas: { + projectId: 'dailymeals-mobile-local', + }, + }, +}); diff --git a/meal-plan-frontend-mobile/app.json b/meal-plan-frontend-mobile/app.json new file mode 100644 index 0000000..c45dbd9 --- /dev/null +++ b/meal-plan-frontend-mobile/app.json @@ -0,0 +1,43 @@ +{ + "expo": { + "name": "meal-plan-frontend-mobile", + "slug": "meal-plan-frontend-mobile", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "mealplanfrontendmobile", + "userInterfaceStyle": "automatic", + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "backgroundColor": "#E6F4FE", + "foregroundImage": "./assets/images/android-icon-foreground.png", + "backgroundImage": "./assets/images/android-icon-background.png", + "monochromeImage": "./assets/images/android-icon-monochrome.png" + }, + "predictiveBackGestureEnabled": false + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + } + ], + "expo-secure-store" + ], + "experiments": { + "typedRoutes": true + } + } +} diff --git a/meal-plan-frontend-mobile/app/(auth)/_layout.tsx b/meal-plan-frontend-mobile/app/(auth)/_layout.tsx new file mode 100644 index 0000000..9f6b77a --- /dev/null +++ b/meal-plan-frontend-mobile/app/(auth)/_layout.tsx @@ -0,0 +1,10 @@ +import { Stack } from 'expo-router'; + +export default function AuthLayout() { + return ( + + + + + ); +} diff --git a/meal-plan-frontend-mobile/app/(auth)/login.tsx b/meal-plan-frontend-mobile/app/(auth)/login.tsx new file mode 100644 index 0000000..bee0c89 --- /dev/null +++ b/meal-plan-frontend-mobile/app/(auth)/login.tsx @@ -0,0 +1,117 @@ +import { Link, router } from 'expo-router'; +import { useState } from 'react'; +import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@/src/components/Button'; +import { Input } from '@/src/components/Input'; +import { Screen } from '@/src/components/Screen'; +import { useAuth } from '@/src/context/AuthContext'; +import { useAppearance } from '@/src/context/AppearanceContext'; +import { extractApiError } from '@/src/utils/apiError'; + +export default function LoginScreen() { + const { t } = useTranslation(); + const { login } = useAuth(); + const { colors } = useAppearance(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit() { + setError(null); + if (!email.trim()) { + setError(t('validation.emailRequired')); + return; + } + if (!password) { + setError(t('validation.passwordRequired')); + return; + } + + setLoading(true); + try { + await login({ email: email.trim(), password }); + router.replace('/(main)/(tabs)'); + } catch (err) { + setError(extractApiError(err, t('errors.signInFailed'))); + } finally { + setLoading(false); + } + } + + return ( + + + + {t('app.name')} + {t('auth.signInSubtitle')} + + + + + {error ? {error} : null} +