Merge branch 'feature/Recipe-Detail-Site' into 'develop'
Recipe-Detail-Site See merge request francesco.damico/francescos.recipes.world!15
This commit is contained in:
@@ -305,8 +305,8 @@
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: /Recipe/{recipeId}/RemoveIngredient/{ingredientId}
|
// DELETE: /Recipe/{recipeId}/RemoveIngredient/{ingredientId}
|
||||||
[HttpPost("{recipeId}/RemoveIngredient/{ingredientId}")]
|
[HttpDelete("{recipeId}/RemoveIngredient/{ingredientId}")]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId)
|
public async Task<IActionResult> RemoveIngredientConfirmed(Guid recipeId, Guid ingredientId)
|
||||||
{
|
{
|
||||||
@@ -338,8 +338,8 @@
|
|||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: /Recipe/{recipeId}/RemoveInstruction/{instructionId}
|
// DELETE: /Recipe/{recipeId}/RemoveInstruction/{instructionId}
|
||||||
[HttpPost("{recipeId}/RemoveInstruction/{instructionId}")]
|
[HttpDelete("{recipeId}/RemoveInstruction/{instructionId}")]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> RemoveInstructionConfirmed(Guid recipeId, Guid instructionId)
|
public async Task<IActionResult> RemoveInstructionConfirmed(Guid recipeId, Guid instructionId)
|
||||||
{
|
{
|
||||||
@@ -470,5 +470,50 @@
|
|||||||
|
|
||||||
return PartialView("_IngredientsPartial", viewModel);
|
return PartialView("_IngredientsPartial", viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: /Recipe/{recipeId}/AdjustableIngredients
|
||||||
|
[HttpGet("{recipeId}/AdjustableIngredients")]
|
||||||
|
public async Task<IActionResult> GetAdjustableIngredients(Guid recipeId)
|
||||||
|
{
|
||||||
|
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
|
||||||
|
if (recipe == null)
|
||||||
|
{
|
||||||
|
return NotFound("Recipe not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return PartialView("_AdjustableIngredientsPartial", recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: /Recipe/{recipeId}/Delete
|
||||||
|
[HttpGet("{recipeId}/Delete")]
|
||||||
|
public async Task<IActionResult> Delete(Guid recipeId)
|
||||||
|
{
|
||||||
|
var recipe = await _recipeRepository.GetRecipeByIdAsync(recipeId);
|
||||||
|
if (recipe == null)
|
||||||
|
{
|
||||||
|
return NotFound("Recipe not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE: /Recipe/{recipeId}/Delete
|
||||||
|
[HttpDelete("{recipeId}/Delete")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteConfirmed(Guid recipeId)
|
||||||
|
{
|
||||||
|
var deleted = await _recipeRepository.DeleteRecipeAsync(recipeId);
|
||||||
|
|
||||||
|
if (deleted)
|
||||||
|
{
|
||||||
|
TempData["SuccessMessage"] = "Rezept wurde erfolgreich gelöscht.";
|
||||||
|
return RedirectToAction("Index", "Home");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["ErrorMessage"] = "Rezept nicht gefunden oder konnte nicht gelöscht werden.";
|
||||||
|
return RedirectToAction("Details", new { recipeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
<Folder Include="Services\Category\" />
|
<Folder Include="Services\Category\" />
|
||||||
<Folder Include="Services\MediaFile\" />
|
<Folder Include="Services\MediaFile\" />
|
||||||
<Folder Include="Services\Ingredient\" />
|
<Folder Include="Services\Ingredient\" />
|
||||||
<Folder Include="Services\Recipe\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
-3
@@ -1,11 +1,8 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
|
||||||
using Francesco.Recipes.World.Data;
|
using Francesco.Recipes.World.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,7 @@
|
|||||||
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
Task<Recipe> CreateRecipeForCategoryAsync(Category category, string name, string description, Difficulty difficulty, int servings, TimeSpan preparationTime, TimeSpan cookingTime);
|
||||||
|
|
||||||
Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchterm);
|
Task<IEnumerable<Recipe>> SearchInRecipesAndIngredients(string searchterm);
|
||||||
|
|
||||||
|
Task<bool> DeleteRecipeAsync(Guid recipeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
.ThenInclude(ri => ri.Unit)
|
.ThenInclude(ri => ri.Unit)
|
||||||
.Include(r => r.MediaFiles)
|
.Include(r => r.MediaFiles)
|
||||||
.Include(r => r.Instructions)
|
.Include(r => r.Instructions)
|
||||||
|
.ThenInclude(i => i.MediaFiles)
|
||||||
.FirstOrDefaultAsync(r => r.Id == recipeId);
|
.FirstOrDefaultAsync(r => r.Id == recipeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,5 +183,48 @@
|
|||||||
.ThenInclude(ri => ri.Ingredient)
|
.ThenInclude(ri => ri.Ingredient)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteRecipeAsync(Guid recipeId)
|
||||||
|
{
|
||||||
|
var recipe = await GetRecipeByIdAsync(recipeId);
|
||||||
|
|
||||||
|
if (recipe == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.RecipeIngredients != null && recipe.RecipeIngredients.Any())
|
||||||
|
{
|
||||||
|
_context.RecipeIngredients.RemoveRange(recipe.RecipeIngredients);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.Instructions != null && recipe.Instructions.Any())
|
||||||
|
{
|
||||||
|
foreach (var instruction in recipe.Instructions)
|
||||||
|
{
|
||||||
|
if (instruction.MediaFiles != null && instruction.MediaFiles.Any())
|
||||||
|
{
|
||||||
|
_context.MediaFiles.RemoveRange(instruction.MediaFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Instructions.RemoveRange(recipe.Instructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.MediaFiles != null && recipe.MediaFiles.Any())
|
||||||
|
{
|
||||||
|
_context.MediaFiles.RemoveRange(recipe.MediaFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipe.Favorit != null && recipe.Favorit.Id != Guid.Empty)
|
||||||
|
{
|
||||||
|
_context.Remove(recipe.Favorit);
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Recipes.Remove(recipe);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
@await Html.PartialAsync("_FavoriteButton", recipe)
|
@await Html.PartialAsync("_FavoriteButton", recipe)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/Recipe/Details/@recipe.Id"
|
<a href="/Details/@recipe.Id"
|
||||||
class="btn btn-primary mt-auto">Details</a>
|
class="btn btn-primary mt-auto">Details</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<h1>@Model.Name</h1>
|
<h1>@Model.Name</h1>
|
||||||
|
|
||||||
<div class="recipe-details">
|
<div class="recipe-details" data-recipe-id="@Model.Id">
|
||||||
<div class="recipe-image">
|
<div class="recipe-image">
|
||||||
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null)
|
@if (Model.MediaFiles.Any() && Model.MediaFiles.First().Data != null)
|
||||||
{
|
{
|
||||||
@@ -20,49 +20,189 @@
|
|||||||
<div class="recipe-info">
|
<div class="recipe-info">
|
||||||
<p><strong>Description:</strong> @Model.Description</p>
|
<p><strong>Description:</strong> @Model.Description</p>
|
||||||
<p><strong>Difficulty:</strong> @Model.Difficulty</p>
|
<p><strong>Difficulty:</strong> @Model.Difficulty</p>
|
||||||
<p><strong>Servings:</strong> @Model.Servings</p>
|
|
||||||
<p><strong>Preparation Time:</strong> @Model.PreparationTime</p>
|
<p><strong>Preparation Time:</strong> @Model.PreparationTime</p>
|
||||||
<p><strong>Cooking Time:</strong> @Model.CookingTime</p>
|
<p><strong>Cooking Time:</strong> @Model.CookingTime</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="recipe-ingredients">
|
|
||||||
<h3>Ingredients</h3>
|
@await Html.PartialAsync("_AdjustableIngredientsPartial", Model)
|
||||||
<form id="ingredient-form">
|
|
||||||
<ul id="ingredient-list">
|
@await Html.PartialAsync("_RecipeInstructionGridPartial", new Francesco.Recipes.World.Models.InstructionViewModel
|
||||||
@foreach (var ingredient in Model.RecipeIngredients)
|
{
|
||||||
{
|
RecipeId = Model.Id,
|
||||||
<li id="ingredient-@ingredient.Id">
|
Instructions = Model.Instructions.ToList()
|
||||||
<input type="checkbox" name="ingredientIds" value="@ingredient.Id" />
|
})
|
||||||
@ingredient.Ingredient.Name - @ingredient.Quantity @(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty)
|
|
||||||
</li>
|
<form hx-post="@($"/{Model.Id}/Delete")"
|
||||||
}
|
hx-confirm="Sind Sie sicher, dass Sie dieses Rezept löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||||
</ul>
|
hx-redirect="/">
|
||||||
<button type="button" class="btn btn-primary" onclick="addSelectedIngredientsToShoppingList()">Add Selected to Shopping List</button>
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash"></i> Rezept löschen
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
async function addSelectedIngredientsToShoppingList() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const recipeId = '@Model.Id';
|
||||||
|
window.recipeId = recipeId;
|
||||||
|
|
||||||
|
const originalServings = @Model.Servings;
|
||||||
|
|
||||||
|
const specialUnits = {
|
||||||
|
discreteUnits: ["stk", "zehe", "blatt", "bund", "stange"],
|
||||||
|
smallUnits: ["msp"],
|
||||||
|
spoonUnits: {
|
||||||
|
"TL": { name: "teelöffel", toMl: 5 },
|
||||||
|
"EL": { name: "esslöffel", toMl: 15 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversionTable = {
|
||||||
|
"knoblauch": {
|
||||||
|
unit: "zehe",
|
||||||
|
smallAmount: 0.5
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const savedServings = localStorage.getItem(`recipe_${recipeId}_servings`);
|
||||||
|
if (savedServings) {
|
||||||
|
document.getElementById('servingsInput').value = savedServings;
|
||||||
|
adjustIngredientQuantities(parseInt(savedServings));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('decreaseServings').addEventListener('click', function () {
|
||||||
|
const input = document.getElementById('servingsInput');
|
||||||
|
const currentValue = parseInt(input.value);
|
||||||
|
if (currentValue > 1) {
|
||||||
|
input.value = currentValue - 1;
|
||||||
|
adjustIngredientQuantities(currentValue - 1);
|
||||||
|
saveServingsToLocalStorage(currentValue - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('increaseServings').addEventListener('click', function () {
|
||||||
|
const input = document.getElementById('servingsInput');
|
||||||
|
const currentValue = parseInt(input.value);
|
||||||
|
input.value = currentValue + 1;
|
||||||
|
adjustIngredientQuantities(currentValue + 1);
|
||||||
|
saveServingsToLocalStorage(currentValue + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('servingsInput').addEventListener('change', function () {
|
||||||
|
const newServings = parseInt(this.value);
|
||||||
|
if (newServings < 1) {
|
||||||
|
this.value = 1;
|
||||||
|
adjustIngredientQuantities(1);
|
||||||
|
saveServingsToLocalStorage(1);
|
||||||
|
} else {
|
||||||
|
adjustIngredientQuantities(newServings);
|
||||||
|
saveServingsToLocalStorage(newServings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function adjustIngredientQuantities(newServings) {
|
||||||
|
const ingredients = document.querySelectorAll('#adjustable-ingredient-list li');
|
||||||
|
|
||||||
|
ingredients.forEach(ingredient => {
|
||||||
|
const originalQuantity = parseFloat(ingredient.getAttribute('data-original-quantity'));
|
||||||
|
const ingredientName = ingredient.getAttribute('data-ingredient-name').toLowerCase();
|
||||||
|
const unitSymbol = ingredient.getAttribute('data-unit');
|
||||||
|
const unitName = ingredient.getAttribute('data-unit-name');
|
||||||
|
|
||||||
|
let adjustedQuantity = (originalQuantity / originalServings) * newServings;
|
||||||
|
let finalQuantity = adjustedQuantity;
|
||||||
|
let note = "";
|
||||||
|
|
||||||
|
if (specialUnits.discreteUnits.includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity < 1 && adjustedQuantity > 0) {
|
||||||
|
finalQuantity = Math.ceil(adjustedQuantity);
|
||||||
|
note = " (ggf. eine kleine/halbe nehmen)";
|
||||||
|
} else {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Convert small unit "msp" (pinch) to teaspoons (TL) for a better estimate
|
||||||
|
// 4 pinches = about 1 TL → show that as a note, rounded to 1 decimal
|
||||||
|
else if (specialUnits.smallUnits.includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity > 3) {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity / 4 * 10) / 10;
|
||||||
|
note = ` (ca. ${finalQuantity} TL)`;
|
||||||
|
finalQuantity = adjustedQuantity;
|
||||||
|
} else {
|
||||||
|
finalQuantity = Math.round(adjustedQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Object.keys(specialUnits.spoonUnits).includes(unitSymbol)) {
|
||||||
|
if (adjustedQuantity > 4 && unitSymbol === "TL") {
|
||||||
|
const esslöffel = Math.round(adjustedQuantity / 3 * 10) / 10;
|
||||||
|
note = ` (ca. ${esslöffel} EL)`;
|
||||||
|
}
|
||||||
|
finalQuantity = Math.round(adjustedQuantity * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ingredientName.includes("knoblauch") && unitSymbol === "zehe" && adjustedQuantity < 1) {
|
||||||
|
finalQuantity = 1;
|
||||||
|
note = " (kleine Zehe)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedQuantity = formatQuantity(finalQuantity);
|
||||||
|
ingredient.querySelector('.ingredient-quantity').textContent = formattedQuantity;
|
||||||
|
|
||||||
|
const noteElement = ingredient.querySelector('.ingredient-note');
|
||||||
|
if (noteElement) {
|
||||||
|
noteElement.textContent = note;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveServingsToLocalStorage(servings) {
|
||||||
|
localStorage.setItem(`recipe_${recipeId}_servings`, servings.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQuantity(quantity) {
|
||||||
|
if (Number.isInteger(quantity)) {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantity === 0.5 || quantity === 0.25 || quantity === 0.75) {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(quantity * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addSelectedIngredientsToShoppingList = function(recipeIdParam) {
|
||||||
|
const idToUse = recipeIdParam || recipeId;
|
||||||
|
|
||||||
var form = document.getElementById('ingredient-form');
|
var form = document.getElementById('ingredient-form');
|
||||||
var formData = new FormData(form);
|
var formData = new FormData(form);
|
||||||
var selectedIngredientIds = formData.getAll('ingredientIds');
|
var selectedIngredientIds = formData.getAll('ingredientIds');
|
||||||
|
|
||||||
var response = await fetch('/ShoppingList/CreateOrAddIngredients', {
|
fetch('/ShoppingList/CreateOrAddIngredients', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds })
|
body: JSON.stringify({ recipeId: idToUse, ingredientIds: selectedIngredientIds })
|
||||||
});
|
})
|
||||||
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
var result = await response.json();
|
return response.json();
|
||||||
|
}
|
||||||
|
throw new Error('Failed to update shopping list');
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
localStorage.setItem('shoppingListId', result.shoppingListId);
|
localStorage.setItem('shoppingListId', result.shoppingListId);
|
||||||
alert('Shopping list updated.');
|
alert('Einkaufsliste aktualisiert.');
|
||||||
} else {
|
})
|
||||||
alert('Failed to update shopping list.');
|
.catch(error => {
|
||||||
}
|
alert('Fehler beim Aktualisieren der Einkaufsliste.');
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@model Francesco.Recipes.World.Models.BackendModels.Recipe.Recipe
|
||||||
|
|
||||||
|
<div class="adjustable-ingredients">
|
||||||
|
<div class="serving-adjustment mb-3" data-original-servings="@Model.Servings">
|
||||||
|
<h3>Ingredients</h3>
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<label for="servingsInput" class="me-2">Adjust servings:</label>
|
||||||
|
<div class="input-group" style="max-width: 150px;">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="decreaseServings">-</button>
|
||||||
|
<input type="number" class="form-control text-center" id="servingsInput" value="@Model.Servings" min="1">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="increaseServings">+</button>
|
||||||
|
</div>
|
||||||
|
<span class="ms-2 text-muted">(Original: @Model.Servings)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="ingredient-form">
|
||||||
|
<ul id="adjustable-ingredient-list">
|
||||||
|
@foreach (var ingredient in Model.RecipeIngredients)
|
||||||
|
{
|
||||||
|
<li id="ingredient-@ingredient.Id"
|
||||||
|
data-ingredient-id="@ingredient.Id"
|
||||||
|
data-original-quantity="@ingredient.Quantity"
|
||||||
|
data-ingredient-name="@ingredient.Ingredient.Name"
|
||||||
|
data-unit="@(ingredient.Unit?.Symbol ?? string.Empty)">
|
||||||
|
<input type="checkbox" name="ingredientIds" value="@ingredient.Id" />
|
||||||
|
<span class="ingredient-name">@ingredient.Ingredient.Name</span> -
|
||||||
|
<span class="ingredient-quantity">@ingredient.Quantity</span>
|
||||||
|
<span class="ingredient-unit">@(ingredient.Unit != null ? ingredient.Unit.Symbol : string.Empty)</span>
|
||||||
|
<span class="ingredient-note"></span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn btn-primary mt-2" onclick="addSelectedIngredientsToShoppingList('@Model.Id')">Add Selected to Shopping List</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||||
<link rel="stylesheet" href="~/Francesco.Recipes.World.styles.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/Francesco.Recipes.World.styles.css" asp-append-version="true" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
@model Francesco.Recipes.World.Models.InstructionViewModel
|
||||||
|
|
||||||
|
<div class="recipe-instructions-section">
|
||||||
|
<h3>Instructions</h3>
|
||||||
|
|
||||||
|
<div class="instructions-grid">
|
||||||
|
@foreach (var instruction in Model.Instructions.OrderBy(i => i.Number))
|
||||||
|
{
|
||||||
|
<div class="instruction-card">
|
||||||
|
<div class="instruction-image">
|
||||||
|
@if (instruction.MediaFiles != null && instruction.MediaFiles.Any())
|
||||||
|
{
|
||||||
|
var mediaFile = instruction.MediaFiles.First();
|
||||||
|
if (mediaFile.Data != null && mediaFile.Data.Length > 0)
|
||||||
|
{
|
||||||
|
<img src="data:@mediaFile.MimeType;base64,@Convert.ToBase64String(mediaFile.Data)" alt="Step @instruction.Number" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="placeholder-image">
|
||||||
|
<i class="bi bi-image"></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<span class="step-number">@instruction.Number</span>
|
||||||
|
</div>
|
||||||
|
<p class="instruction-text">@instruction.Description</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -85,3 +85,102 @@ textarea.form-control {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Recipe Instructions Grid Layout */
|
||||||
|
.instructions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.instructions-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.instructions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-card {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #999;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction-text {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-instructions-section h3 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-instructions-section h3:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -189,3 +189,25 @@ async function addIngredient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function addSelectedIngredientsToShoppingList() {
|
||||||
|
var form = document.getElementById('ingredient-form');
|
||||||
|
var formData = new FormData(form);
|
||||||
|
var selectedIngredientIds = formData.getAll('ingredientIds');
|
||||||
|
|
||||||
|
var response = await fetch('/ShoppingList/CreateOrAddIngredients', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ recipeId: '@Model.Id', ingredientIds: selectedIngredientIds })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
var result = await response.json();
|
||||||
|
localStorage.setItem('shoppingListId', result.shoppingListId);
|
||||||
|
alert('Shopping list updated.');
|
||||||
|
} else {
|
||||||
|
alert('Failed to update shopping list.');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user