Recept och bilder
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-05-22 22:15:33 +02:00
parent addef3a3ad
commit bd8c3a0fab
6 changed files with 213 additions and 77 deletions

View File

@@ -94,7 +94,6 @@ namespace Aberwyn.Controllers
}
else
{
// ✅ Hämta tidigare måltid och kopiera bilddata
var existingMeal = service.GetMealById(meal.Id);
if (existingMeal != null)
{
@@ -103,13 +102,17 @@ namespace Aberwyn.Controllers
}
}
service.SaveOrUpdateMeal(meal);
// 🔁 Här är ändringen använd metoden som även sparar ingredienser
service.SaveOrUpdateMealWithIngredients(meal);
return RedirectToAction("View", new { id = meal.Id });
}
[HttpPost]
public IActionResult DeleteMeal(int id)
{

View File

@@ -134,6 +134,55 @@ namespace Aberwyn.Data
return weeklyMenu;
}
public List<Ingredient> GetIngredientsForMeal(int mealId)
{
var ingredients = new List<Ingredient>();
using var conn = GetConnection();
conn.Open();
string query = "SELECT Id, MealId, Quantity, Item FROM Ingredients WHERE MealId = @mealId";
using var cmd = new MySqlCommand(query, conn);
cmd.Parameters.AddWithValue("@mealId", mealId);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
ingredients.Add(new Ingredient
{
Id = reader.GetInt32("Id"),
MealId = reader.GetInt32("MealId"),
Quantity = reader.GetString("Quantity"),
Item = reader.GetString("Item")
});
}
return ingredients;
}
public void SaveIngredients(int mealId, List<Ingredient> ingredients)
{
using var conn = GetConnection();
conn.Open();
using var tx = conn.BeginTransaction();
// Ta bort gamla
var deleteCmd = new MySqlCommand("DELETE FROM Ingredients WHERE MealId = @mealId", conn, tx);
deleteCmd.Parameters.AddWithValue("@mealId", mealId);
deleteCmd.ExecuteNonQuery();
foreach (var ingredient in ingredients)
{
var insertCmd = new MySqlCommand(
"INSERT INTO Ingredients (MealId, Quantity, Item) VALUES (@mealId, @quantity, @item)",
conn, tx);
insertCmd.Parameters.AddWithValue("@mealId", mealId);
insertCmd.Parameters.AddWithValue("@quantity", ingredient.Quantity ?? "");
insertCmd.Parameters.AddWithValue("@item", ingredient.Item ?? "");
insertCmd.ExecuteNonQuery();
}
tx.Commit();
}
public List<Meal> GetMeals()
{
@@ -205,7 +254,7 @@ namespace Aberwyn.Data
using var connection = GetConnection();
connection.Open();
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType FROM Meals WHERE Id = @id";
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions FROM Meals WHERE Id = @id";
using var cmd = new MySqlCommand(query, connection);
cmd.Parameters.AddWithValue("@id", id);
@@ -224,8 +273,9 @@ namespace Aberwyn.Data
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")),
ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"],
ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType"))
ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")),
Instructions = reader.IsDBNull(reader.GetOrdinal("Instructions")) ? null : reader.GetString(reader.GetOrdinal("Instructions")),
Ingredients = GetIngredientsForMeal(id)
};
@@ -254,9 +304,9 @@ namespace Aberwyn.Data
{
cmd.CommandText = @"
INSERT INTO Meals
(Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType)
(Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions)
VALUES
(@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType);
(@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType, @Instructions);
SELECT LAST_INSERT_ID();";
}
else
@@ -270,7 +320,8 @@ namespace Aberwyn.Data
RecipeUrl = @RecipeUrl,
ImageUrl = @ImageUrl,
ImageData = @ImageData,
ImageMimeType = @ImageMimeType
ImageMimeType = @ImageMimeType,
Instructions = @Instructions
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", meal.Id);
}
@@ -282,10 +333,9 @@ namespace Aberwyn.Data
cmd.Parameters.AddWithValue("@RecipeUrl", meal.RecipeUrl ?? "");
cmd.Parameters.AddWithValue("@CreatedAt", meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt);
cmd.Parameters.AddWithValue("@ImageUrl", meal.ImageUrl ?? "");
// ✨ Här är nyckeln
cmd.Parameters.AddWithValue("@ImageData", (object?)meal.ImageData ?? DBNull.Value);
cmd.Parameters.AddWithValue("@ImageMimeType", meal.ImageMimeType ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@Instructions", meal.Instructions ?? "");
if (meal.Id == 0)
{
@@ -298,9 +348,6 @@ namespace Aberwyn.Data
}
}
public void UpdateWeeklyMenu(MenuViewModel menuData)
{
if (menuData == null || menuData.WeeklyMenus == null)
@@ -413,6 +460,17 @@ namespace Aberwyn.Data
_ => throw new System.ArgumentException("Invalid day name")
};
}
public void SaveOrUpdateMealWithIngredients(Meal meal)
{
// Spara/uppdatera måltid
SaveOrUpdateMeal(meal);
// Om måltiden har ingredienser spara dem
if (meal.Ingredients != null && meal.Ingredients.Count > 0)
{
SaveIngredients(meal.Id, meal.Ingredients);
}
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
var results = new List<WeeklyMenu>();

View File

@@ -46,7 +46,15 @@ namespace Aberwyn.Models
public DateTime CreatedAt { get; set; }
public byte[] ImageData { get; set; }
public string ImageMimeType { get; set; } // t.ex. "image/jpeg"
public string Instructions { get; set; } // 👈 Tillagningstext
public List<Ingredient> Ingredients { get; set; } = new();
}
public class Ingredient
{
public int Id { get; set; }
public int MealId { get; set; }
public string Quantity { get; set; }
public string Item { get; set; }
}
public class MealDto

View File

@@ -151,4 +151,8 @@
}
}
});
const modalImageInput = document.getElementById("modalImageInput");
</script>

View File

@@ -1,4 +1,7 @@
@model Aberwyn.Models.Meal
@using Microsoft.AspNetCore.Authorization
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
@{
ViewData["Title"] = Model.Name;
bool isEditing = (bool)(ViewData["IsEditing"] ?? false);
@@ -14,13 +17,21 @@
imageSrc = "/images/placeholder-meal.jpg";
}
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
isEditing = isEditing && isChef;
}
<div class="meal-container">
<div class="meal-header">
<div id="imageDropArea" class="meal-image-wrapper" title="Klicka för att ladda upp bild">
<div class="meal-image-wrapper clickable" title="Bild på rätten">
<img id="imagePreview" src="@imageSrc" alt="@Model.Name" class="meal-image" />
</div>
@if (isEditing)
{
<div style="margin-top: 0.5rem;">
<button type="button" class="btn-outline" onclick="openImageModal()">Byt bild</button>
</div>
}
<div class="meal-meta">
<h1 class="meal-title">@Model.Name</h1>
@@ -32,8 +43,6 @@
{
<form asp-action="SaveMeal" method="post" enctype="multipart/form-data">
<input type="hidden" name="Id" value="@Model.Id" />
<input type="hidden" name="ExistingImageUrl" value="@Model.ImageUrl" />
<input type="file" id="ImageFile" name="ImageFile" accept="image/*" style="display: none;" />
<div class="form-group">
@@ -61,6 +70,27 @@
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" class="form-control" />
</div>
<div id="recipe-section">
<h2>Så här gör du</h2>
<div class="form-group">
<label for="Instructions">Tillagningsinstruktioner</label>
<textarea id="Instructions" name="Instructions">@Html.Raw(Model.Instructions)</textarea>
</div>
<div class="form-group">
<label>Ingredienser</label>
<div id="ingredients-list">
@for (int i = 0; i < Model.Ingredients.Count; i++)
{
<div class="ingredient-row">
<input type="text" name="Ingredients[@i].Quantity" placeholder="Mängd" value="@Model.Ingredients[i].Quantity" class="form-control ingredient-qty" />
<input type="text" name="Ingredients[@i].Item" placeholder="Ingrediens" value="@Model.Ingredients[i].Item" class="form-control ingredient-item" />
</div>
}
</div>
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till ingrediens</button>
</div>
</div>
<div class="buttons">
<button type="submit" class="btn">Spara</button>
<button type="submit" formaction="@Url.Action("DeleteMeal", new { id = Model.Id })" formmethod="post" onclick="return confirm('Vill du verkligen ta bort denna måltid?');" class="btn-outline">Ta bort</button>
@@ -87,100 +117,118 @@
}
<div class="buttons">
<button type="button" class="btn-outline" onclick="toggleRecipe()">Visa Tillagning</button>
<a class="btn-outline" href="@Url.Action("View", new { id = Model.Id, edit = true })">Redigera</a>
@if (isChef)
{
<a class="btn-outline" href="@Url.Action("View", new { id = Model.Id, edit = true })">Redigera</a>
}
<button type="button" class="btn-outline" onclick="toggleRecipe()">Recept</button>
</div>
<div id="recipe-section" style="display:none; margin-top:1.5rem;">
<h2>Så här gör du</h2>
@if (!string.IsNullOrWhiteSpace(Model.Instructions))
{
<div class="instructions-content">
@Html.Raw(Model.Instructions)
</div>
}
else
{
<p class="placeholder">Ingen instruktion har lagts till ännu.</p>
}
@if (Model.Ingredients != null && Model.Ingredients.Any())
{
<h3>Ingredienser</h3>
<ul>
@foreach (var ing in Model.Ingredients)
{
<li>@ing.Quantity @ing.Item</li>
}
</ul>
}
</div>
}
<div id="recipe-section" style="display:none; margin-top:1.5rem;">
<h2>Så här gör du</h2>
<p class="placeholder">(Tillagningsinstruktioner kommer snart...)</p>
@if (!string.IsNullOrEmpty(Model.RecipeUrl))
{
<p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Gå till fullständigt recept</a></p>
}
</div>
</div>
<!-- Modal -->
<div id="imageModal" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 1000;">
<div style="background: white; margin: 5% auto; padding: 2rem; max-width: 400px; border-radius: 8px; text-align: center; position: relative;">
<div id="imageModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1000;">
<div style="background:white; margin:5% auto; padding:2rem; max-width:400px; border-radius:8px; text-align:center; position:relative;">
<h3>Klistra in eller dra in bild</h3>
<div id="dropZone" style="border: 2px dashed #aaa; padding: 2rem; cursor: pointer;">
<div id="dropZone" style="border:2px dashed #aaa; padding:2rem; cursor:pointer;">
<p>Släpp bild här eller klistra in (Ctrl+V)</p>
</div>
<button onclick="closeModal()" style="margin-top: 1rem;">Stäng</button>
<input type="file" id="modalImageInput" accept="image/*" style="display:none;" />
<button type="button" onclick="document.getElementById('modalImageInput').click()">Välj bild från enhet</button>
<button onclick="closeImageModal()" style="margin-top:1rem;">Stäng</button>
</div>
</div>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/ui/trumbowyg.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/trumbowyg.min.js"></script>
<script>
function toggleRecipe() {
const section = document.getElementById('recipe-section');
section.style.display = section.style.display === 'none' ? 'block' : 'none';
}
function toggleRecipe() {
const section = document.getElementById('recipe-section');
if (!section) return;
const imageFileInput = document.getElementById("ImageFile");
const imageDropArea = document.getElementById("imageDropArea");
const imagePreview = document.getElementById("imagePreview");
const imageModal = document.getElementById("imageModal");
const dropZone = document.getElementById("dropZone");
if (imageDropArea && @isEditing.ToString().ToLower()) {
imageDropArea.style.cursor = "pointer";
imageDropArea.addEventListener("click", () => {
imageModal.style.display = "block";
});
function closeModal() {
imageModal.style.display = "none";
if (section.style.display === 'none' || section.style.display === '') {
section.style.display = 'block';
} else {
section.style.display = 'none';
}
}
function openImageModal() {
document.getElementById('imageModal').style.display = 'block';
}
function closeImageModal() {
document.getElementById('imageModal').style.display = 'none';
}
function handleImage(file) {
const reader = new FileReader();
reader.onload = e => {
imagePreview.src = e.target.result;
document.getElementById("imagePreview").src = e.target.result;
const dt = new DataTransfer();
dt.items.add(file);
imageFileInput.files = dt.files;
document.getElementById("ImageFile").files = dt.files;
};
reader.readAsDataURL(file);
}
dropZone.addEventListener("dragover", e => {
document.getElementById('dropZone').addEventListener('dragover', e => {
e.preventDefault();
dropZone.style.borderColor = "#4A90E2";
e.currentTarget.style.borderColor = '#6a0dad';
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.borderColor = "#aaa";
document.getElementById('dropZone').addEventListener('dragleave', e => {
e.currentTarget.style.borderColor = '#aaa';
});
dropZone.addEventListener("drop", e => {
document.getElementById('dropZone').addEventListener('drop', e => {
e.preventDefault();
dropZone.style.borderColor = "#aaa";
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) {
if (file && file.type.startsWith('image/')) {
handleImage(file);
closeModal();
closeImageModal();
}
});
document.addEventListener("paste", e => {
if (imageModal.style.display !== "block") return;
for (const item of e.clipboardData.items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
handleImage(file);
closeModal();
}
document.getElementById('modalImageInput').addEventListener('change', e => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
handleImage(file);
closeImageModal();
}
});
$(document).ready(function () {
if ($('#Instructions').length > 0) {
$('#Instructions').trumbowyg({
autogrow: true,
btns: [['strong', 'em'], ['unorderedList', 'orderedList'], ['link'], ['removeformat']]
});
}
});
}
</script>
<style>
.dragging {
outline: 2px dashed #6a0dad;
}
.meal-image-wrapper {
position: relative;
}
@@ -274,4 +322,18 @@ if (imageDropArea && @isEditing.ToString().ToLower()) {
color: #999;
font-style: italic;
}
.ingredient-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.ingredient-qty {
flex: 1 0 30%;
}
.ingredient-item {
flex: 2 0 60%;
}
</style>

View File

@@ -269,3 +269,4 @@ h1 {
.top-buttons button:hover {
background-color: rgba(0, 0, 0, 0.1);
}