Menu and meal upgrades, fixed base64 images
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
380
Aberwyn/Migrations/20250522074358_AddMealImageData.Designer.cs
generated
Normal file
380
Aberwyn/Migrations/20250522074358_AddMealImageData.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Aberwyn/Migrations/20250522074358_AddMealImageData.cs
Normal file
19
Aberwyn/Migrations/20250522074358_AddMealImageData.cs
Normal 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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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...");
|
||||
|
||||
Reference in New Issue
Block a user