Menu and meal upgrades, fixed base64 images
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-05-22 19:19:51 +02:00
parent f824e2d3b6
commit addef3a3ad
12 changed files with 701 additions and 89 deletions

View File

@@ -81,33 +81,35 @@ namespace Aberwyn.Controllers
}
[HttpPost]
public IActionResult SaveMeal(Meal meal, IFormFile ImageFile)
public IActionResult SaveMeal(Meal meal, IFormFile ImageFile, string ExistingImageUrl)
{
var service = new MenuService(_configuration, _env);
if (ImageFile != null && ImageFile.Length > 0)
{
var fileName = Path.GetFileNameWithoutExtension(ImageFile.FileName);
var extension = Path.GetExtension(ImageFile.FileName);
var uniqueName = $"{fileName}_{Guid.NewGuid()}{extension}";
var imagePath = Path.Combine("wwwroot/images/meals", uniqueName);
using (var stream = new FileStream(imagePath, FileMode.Create))
using var ms = new MemoryStream();
ImageFile.CopyTo(ms);
meal.ImageData = ms.ToArray();
meal.ImageMimeType = ImageFile.ContentType;
}
else
{
// ✅ Hämta tidigare måltid och kopiera bilddata
var existingMeal = service.GetMealById(meal.Id);
if (existingMeal != null)
{
ImageFile.CopyTo(stream);
meal.ImageData = existingMeal.ImageData;
meal.ImageMimeType = existingMeal.ImageMimeType;
}
// Spara relativ sökväg för visning
meal.ImageUrl = $"/images/meals/{uniqueName}";
}
service.SaveOrUpdateMeal(meal);
return RedirectToAction("View", new { id = meal.Id });
}
[HttpPost]
public IActionResult DeleteMeal(int id)
{

View File

@@ -28,10 +28,12 @@ namespace Aberwyn.Controllers
[HttpGet("getMeals")]
public IActionResult GetMeals()
{
var meals = _menuService.GetMeals();
return Ok(meals ?? new List<Meal>());
var meals = _menuService.GetMealsDetailed(); // Hämtar med ImageData
var mealDtos = meals.Select(MealDto.FromMeal).ToList();
return Ok(mealDtos);
}
[HttpPut("menu")]
public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu)
{

View File

@@ -141,7 +141,7 @@ namespace Aberwyn.Data
using (var connection = GetConnection())
{
connection.Open();
string query = "SELECT Id, Name, ImageUrl FROM Meals";
string query = "SELECT Id, Name, ImageUrl, ImageData, ImageMimeType FROM Meals";
using (var cmd = new MySqlCommand(query, connection))
{
using (var reader = cmd.ExecuteReader())
@@ -152,6 +152,8 @@ namespace Aberwyn.Data
{
Id = reader.GetInt32("Id"),
Name = reader.GetString("Name"),
ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"],
ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")),
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl"))
});
}
@@ -168,7 +170,7 @@ namespace Aberwyn.Data
{
connection.Open();
string query = @"
SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl
SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType
FROM Meals
ORDER BY CreatedAt DESC";
@@ -186,7 +188,10 @@ namespace Aberwyn.Data
CarbType = reader.IsDBNull(reader.GetOrdinal("CarbType")) ? null : reader.GetString(reader.GetOrdinal("CarbType")),
RecipeUrl = reader.IsDBNull(reader.GetOrdinal("RecipeUrl")) ? null : reader.GetString(reader.GetOrdinal("RecipeUrl")),
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
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"))
});
}
}
@@ -200,8 +205,7 @@ namespace Aberwyn.Data
using var connection = GetConnection();
connection.Open();
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl
FROM Meals WHERE Id = @id";
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType FROM Meals WHERE Id = @id";
using var cmd = new MySqlCommand(query, connection);
cmd.Parameters.AddWithValue("@id", id);
@@ -218,7 +222,11 @@ namespace Aberwyn.Data
CarbType = reader.IsDBNull(reader.GetOrdinal("CarbType")) ? null : reader.GetString(reader.GetOrdinal("CarbType")),
RecipeUrl = reader.IsDBNull(reader.GetOrdinal("RecipeUrl")) ? null : reader.GetString(reader.GetOrdinal("RecipeUrl")),
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
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"))
};
}
@@ -244,16 +252,25 @@ namespace Aberwyn.Data
using var cmd = conn.CreateCommand();
if (meal.Id == 0)
{
cmd.CommandText = @"INSERT INTO Meals
(Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl)
VALUES (@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl);
cmd.CommandText = @"
INSERT INTO Meals
(Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType)
VALUES
(@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType);
SELECT LAST_INSERT_ID();";
}
else
{
cmd.CommandText = @"UPDATE Meals
SET Name = @Name, Description = @Description, ProteinType = @ProteinType,
CarbType = @CarbType, RecipeUrl = @RecipeUrl, ImageUrl = @ImageUrl
cmd.CommandText = @"
UPDATE Meals
SET Name = @Name,
Description = @Description,
ProteinType = @ProteinType,
CarbType = @CarbType,
RecipeUrl = @RecipeUrl,
ImageUrl = @ImageUrl,
ImageData = @ImageData,
ImageMimeType = @ImageMimeType
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", meal.Id);
}
@@ -266,10 +283,14 @@ namespace Aberwyn.Data
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);
if (meal.Id == 0)
{
var insertedId = Convert.ToInt32(cmd.ExecuteScalar());
meal.Id = insertedId; // ← Sätt ID direkt på objektet
meal.Id = insertedId;
}
else
{

View File

@@ -0,0 +1,380 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250522074358_AddMealImageData")]
partial class AddMealImageData
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetCategory");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddMealImageData : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -44,6 +44,31 @@ namespace Aberwyn.Models
public string RecipeUrl { get; set; }
public string ImageUrl { get; set; }
public DateTime CreatedAt { get; set; }
public byte[] ImageData { get; set; }
public string ImageMimeType { get; set; } // t.ex. "image/jpeg"
}
public class MealDto
{
public int Id { get; set; }
public string Name { get; set; }
public string? ImageUrl { get; set; }
public string? ImageData { get; set; } // base64
public string? ImageMimeType { get; set; }
public static MealDto FromMeal(Meal meal)
{
return new MealDto
{
Id = meal.Id,
Name = meal.Name,
ImageUrl = meal.ImageUrl,
ImageMimeType = meal.ImageMimeType,
ImageData = meal.ImageData != null ? Convert.ToBase64String(meal.ImageData) : null
};
}
}
}

View File

@@ -7,45 +7,59 @@
<div class="meal-details">
<h1>@(isNew ? "Skapa ny måltid" : Model.Name)</h1>
<form asp-action="SaveMeal" method="post" enctype="multipart/form-data">
@if (!isNew)
{
<input type="hidden" name="Id" value="@Model.Id" />
}
<form asp-action="SaveMeal" method="post" enctype="multipart/form-data">
@if (!isNew)
{
<input type="hidden" name="Id" value="@Model.Id" />
}
<div>
<label for="Name">Namn</label>
<input type="text" name="Name" value="@Model.Name" required />
<div>
<label for="Name">Namn</label>
<input type="text" name="Name" value="@Model.Name" required />
</div>
<div>
<label for="Description">Beskrivning</label>
<textarea name="Description">@Model.Description</textarea>
</div>
<div>
<label for="ProteinType">Protein</label>
<input type="text" name="ProteinType" value="@Model.ProteinType" />
</div>
<div>
<label for="CarbType">Kolhydrat</label>
<input type="text" name="CarbType" value="@Model.CarbType" />
</div>
<div>
<label for="RecipeUrl">Receptlänk</label>
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" />
</div>
<div>
<label for="ImageFile">Bild</label>
<div id="imageDropArea" style="border: 2px dashed #ccc; padding: 1rem; text-align: center; cursor: pointer;">
<img id="imagePreview" src="@Model.ImageUrl" alt="Förhandsvisning" style="max-width: 100%; display: @(string.IsNullOrEmpty(Model.ImageUrl) ? "none" : "block");" />
<span id="imagePlaceholder" style="color: #888;">Klicka här för att klistra in eller dra in en bild</span>
</div>
<input type="file" id="ImageFile" name="ImageFile" accept="image/*" style="display: none;" />
</div>
<button type="submit">Spara</button>
</form>
</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;">
<h3>Klistra in eller dra in bild</h3>
<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>
</div>
<div>
<label for="Description">Beskrivning</label>
<textarea name="Description">@Model.Description</textarea>
</div>
<div>
<label for="ProteinType">Protein</label>
<input type="text" name="ProteinType" value="@Model.ProteinType" />
</div>
<div>
<label for="CarbType">Kolhydrat</label>
<input type="text" name="CarbType" value="@Model.CarbType" />
</div>
<div>
<label for="RecipeUrl">Receptlänk</label>
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" />
</div>
<div>
<label for="ImageFile">Bild</label>
<input type="file" name="ImageFile" value="@Model.ImageUrl" />
</div>
<button type="submit">Spara</button>
</form>
</div>
<style>
@@ -56,11 +70,13 @@
padding: 2rem;
border-radius: 8px;
}
.meal-details label {
display: block;
font-weight: bold;
margin-top: 1rem;
}
.meal-details input[type=text],
.meal-details input[type=url],
.meal-details textarea {
@@ -70,8 +86,69 @@
border: 1px solid #ccc;
margin-top: 0.25rem;
}
.meal-details button {
margin-top: 1rem;
margin-right: 1rem;
}
</style>
<script>
const imageFileInput = document.getElementById("ImageFile");
const imageDropArea = document.getElementById("imageDropArea");
const imagePreview = document.getElementById("imagePreview");
const imagePlaceholder = document.getElementById("imagePlaceholder");
const imageModal = document.getElementById("imageModal");
const dropZone = document.getElementById("dropZone");
imageDropArea.addEventListener("click", () => {
imageModal.style.display = "block";
});
function closeModal() {
imageModal.style.display = "none";
}
function handleImage(file) {
const reader = new FileReader();
reader.onload = e => {
imagePreview.src = e.target.result;
imagePreview.style.display = "block";
imagePlaceholder.style.display = "none";
const dt = new DataTransfer();
dt.items.add(file);
imageFileInput.files = dt.files;
};
reader.readAsDataURL(file);
}
dropZone.addEventListener("dragover", e => {
e.preventDefault();
dropZone.style.borderColor = "#4A90E2";
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.borderColor = "#aaa";
});
dropZone.addEventListener("drop", e => {
e.preventDefault();
dropZone.style.borderColor = "#aaa";
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) {
handleImage(file);
closeModal();
}
});
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();
}
}
});
</script>

View File

@@ -2,12 +2,26 @@
@{
ViewData["Title"] = Model.Name;
bool isEditing = (bool)(ViewData["IsEditing"] ?? false);
var imageUrl = string.IsNullOrEmpty(Model.ImageUrl) ? "/images/placeholder-meal.jpg" : Model.ImageUrl;
string imageSrc;
if (Model.ImageData != null && Model.ImageData.Length > 0)
{
var base64 = Convert.ToBase64String(Model.ImageData);
var mime = string.IsNullOrEmpty(Model.ImageMimeType) ? "image/jpeg" : Model.ImageMimeType;
imageSrc = $"data:{mime};base64,{base64}";
}
else
{
imageSrc = "/images/placeholder-meal.jpg";
}
}
<div class="meal-container">
<div class="meal-header">
<img src="@imageUrl" alt="@Model.Name" class="meal-image" />
<div id="imageDropArea" class="meal-image-wrapper" title="Klicka för att ladda upp bild">
<img id="imagePreview" src="@imageSrc" alt="@Model.Name" class="meal-image" />
</div>
<div class="meal-meta">
<h1 class="meal-title">@Model.Name</h1>
<p class="description">@Model.Description</p>
@@ -18,11 +32,9 @@
{
<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" />
<div class="form-group">
<label for="ImageFile">Bild</label>
<input type="file" name="ImageFile" accept="image/*" value="@Model.ImageUrl" class="form-control" />
</div>
<input type="file" id="ImageFile" name="ImageFile" accept="image/*" style="display: none;" />
<div class="form-group">
<label for="Name">Namn</label>
@@ -91,14 +103,87 @@
</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;">
<h3>Klistra in eller dra in bild</h3>
<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>
</div>
</div>
<script>
function toggleRecipe() {
const section = document.getElementById('recipe-section');
section.style.display = section.style.display === 'none' ? 'block' : 'none';
}
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";
}
function handleImage(file) {
const reader = new FileReader();
reader.onload = e => {
imagePreview.src = e.target.result;
const dt = new DataTransfer();
dt.items.add(file);
imageFileInput.files = dt.files;
};
reader.readAsDataURL(file);
}
dropZone.addEventListener("dragover", e => {
e.preventDefault();
dropZone.style.borderColor = "#4A90E2";
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.borderColor = "#aaa";
});
dropZone.addEventListener("drop", e => {
e.preventDefault();
dropZone.style.borderColor = "#aaa";
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith("image/")) {
handleImage(file);
closeModal();
}
});
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();
}
}
});
}
</script>
<style>
.meal-image-wrapper {
position: relative;
}
.meal-container {
max-width: 900px;
margin: 2rem auto;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -15,6 +15,10 @@ angular.module('mealMenuApp', ['ngSanitize'])
$scope.selectedWeek = getWeek(today);
$scope.selectedYear = today.getFullYear();
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
const savedViewMode = localStorage.getItem('mealViewMode');
if (savedViewMode === 'list' || savedViewMode === 'card') {
$scope.viewMode = savedViewMode;
}
$scope.loadMeals = function () {
console.log("Hämtar måltider...");
@@ -53,17 +57,23 @@ angular.module('mealMenuApp', ['ngSanitize'])
const nameKey = capitalType + 'MealName';
if (item[idKey]) {
const m = $scope.meals.find(x => x.Id === item[idKey]);
const m = $scope.meals.find(x => x.Id == item[idKey]);
console.log(`Match för ${type} (${day}):`, m);
$scope.menu[day][type + 'MealId'] = item[idKey];
$scope.menu[day][type + 'MealName'] = m?.Name || item[nameKey] || 'Okänd rätt';
if (m?.ImageUrl && !$scope.menu[day].imageUrl) {
$scope.menu[day].imageUrl = m.ImageUrl.startsWith('/')
? m.ImageUrl
: '/' + m.ImageUrl;
if (m && !$scope.menu[day].imageUrl) {
if (m.ImageData) {
const mime = m.ImageMimeType || "image/jpeg";
$scope.menu[day].imageUrl = `data:${mime};base64,${m.ImageData}`;
} else if (m.ImageUrl) {
$scope.menu[day].imageUrl = m.ImageUrl.startsWith('/')
? m.ImageUrl
: '/' + m.ImageUrl;
}
}
}
});
});
@@ -78,18 +88,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
};
$scope.getDayImage = function (day) {
const item = $scope.menu[day];
const img = item?.imageUrl;
if (img && img.startsWith('/')) {
return img;
}
if (img && !img.startsWith('/')) {
return '/' + img; // Fixa ev. saknad inledande snedstreck
}
return '/images/default-meal.jpg';
return $scope.menu[day]?.imageUrl || '/images/default-meal.jpg';
};
$scope.getMealIdByDay = function (day) {
@@ -130,7 +129,8 @@ angular.module('mealMenuApp', ['ngSanitize'])
$scope.toggleView = function () {
$scope.viewMode = $scope.viewMode === 'list' ? 'card' : 'list';
// Uppdatera knappens ikon
localStorage.setItem('mealViewMode', $scope.viewMode); // ← spara läget
setTimeout(() => {
const btn = document.getElementById('toggle-view');
if (btn) {
@@ -140,6 +140,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
};
console.log("Initierar måltidsladdning...");
$scope.loadMeals().then(() => {
console.log("Laddar meny efter måltider...");