Meal rating and some fixes to meals
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-07-07 15:22:12 +02:00
parent 380978959b
commit 8ebbb803e8
14 changed files with 1770 additions and 249 deletions

View File

@@ -28,10 +28,10 @@ namespace Aberwyn.Controllers
} }
[HttpGet("getPublishedMeals")] [HttpGet("getPublishedMeals")]
public IActionResult GetPublishedMeals() public IActionResult GetPublishedMeals([FromQuery] bool includeUnpublished = false)
{ {
var meals = _menuService.GetMeals() var meals = _menuService.GetMeals()
.Where(m => m.IsPublished) // 🟢 filtrera här! .Where(m => includeUnpublished || m.IsPublished)
.Select(m => new { .Select(m => new {
m.Id, m.Id,
m.Name, m.Name,
@@ -44,6 +44,7 @@ namespace Aberwyn.Controllers
} }
[HttpGet("getMeals")] [HttpGet("getMeals")]
public IActionResult GetMeals() public IActionResult GetMeals()
{ {

View File

@@ -0,0 +1,79 @@
using Aberwyn.Data;
using Aberwyn.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Aberwyn.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MealRatingApiController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
public MealRatingApiController(ApplicationDbContext context, UserManager<ApplicationUser> userManager)
{
_context = context;
_userManager = userManager;
}
[HttpGet("{mealId}")]
public async Task<IActionResult> GetRating(int mealId)
{
var user = await _userManager.GetUserAsync(User);
if (user == null) return Unauthorized();
var rating = await _context.MealRatings
.FirstOrDefaultAsync(r => r.MealId == mealId && r.UserId == user.Id);
return Ok(rating?.Rating ?? 0);
}
[HttpGet("average/{mealId}")]
public async Task<IActionResult> GetAverageRating(int mealId)
{
var ratings = await _context.MealRatings
.Where(r => r.MealId == mealId)
.ToListAsync();
if (ratings.Count == 0)
return Ok(0);
var avg = ratings.Average(r => r.Rating);
return Ok(avg);
}
[HttpPost]
public async Task<IActionResult> SetRating([FromBody] MealRatingDto model)
{
var user = await _userManager.GetUserAsync(User);
if (user == null) return Unauthorized();
var existing = await _context.MealRatings
.FirstOrDefaultAsync(r => r.MealId == model.MealId && r.UserId == user.Id);
if (existing != null)
{
existing.Rating = model.Rating;
existing.CreatedAt = DateTime.UtcNow;
}
else
{
_context.MealRatings.Add(new MealRating
{
MealId = model.MealId,
UserId = user.Id,
Rating = model.Rating,
CreatedAt = DateTime.UtcNow
});
}
await _context.SaveChangesAsync();
return Ok();
}
}
}

View File

@@ -51,6 +51,7 @@ namespace Aberwyn.Data
public DbSet<RecipeLabVersion> RecipeLabVersions { get; set; } public DbSet<RecipeLabVersion> RecipeLabVersions { get; set; }
public DbSet<LabIngredient> LabIngredients { get; set; } public DbSet<LabIngredient> LabIngredients { get; set; }
public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; } public DbSet<LabVersionIngredient> LabVersionIngredients { get; set; }
public DbSet<MealRating> MealRatings { get; set; }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddMealRating : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MealRatings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
MealId = table.Column<int>(type: "int", nullable: false),
UserId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Rating = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MealRatings", x => x.Id);
table.ForeignKey(
name: "FK_MealRatings_Meals_MealId",
column: x => x.MealId,
principalTable: "Meals",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_MealRatings_MealId",
table: "MealRatings",
column: "MealId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MealRatings");
}
}
}

View File

@@ -413,6 +413,32 @@ namespace Aberwyn.Migrations
}); });
}); });
modelBuilder.Entity("Aberwyn.Models.MealRating", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("MealRatings");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -868,6 +894,17 @@ namespace Aberwyn.Migrations
b.Navigation("Category"); b.Navigation("Category");
}); });
modelBuilder.Entity("Aberwyn.Models.MealRating", b =>
{
b.HasOne("Aberwyn.Models.Meal", "Meal")
.WithMany()
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Meal");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{ {
b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder") b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder")

View File

@@ -177,5 +177,22 @@ public class WeeklyMenu
public List<Meal> Meals { get; set; } = new(); public List<Meal> Meals { get; set; } = new();
} }
public class MealRating
{
public int Id { get; set; }
public int MealId { get; set; }
public string UserId { get; set; }
public int Rating { get; set; }
public DateTime CreatedAt { get; set; }
public Meal Meal { get; set; }
}
public class MealRatingDto
{
public int MealId { get; set; }
public int Rating { get; set; }
}
} }

View File

@@ -19,44 +19,51 @@
{ {
<div class="meal-lines"> <div class="meal-lines">
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) { @if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) {
<div class="meal-line"> <div class="meal-line">
@if (Model.BreakfastThumbnail != null) <p><strong>Frukost:</strong>
{ <a href="/meal/view/@Model.BreakfastMealId">@Model.BreakfastMealName</a>
var b64 = Convert.ToBase64String(Model.BreakfastThumbnail); </p>
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.BreakfastMealName')"> @if (Model.BreakfastThumbnail != null)
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.BreakfastMealName" /> {
</button> var b64 = Convert.ToBase64String(Model.BreakfastThumbnail);
} <div class="meal-large-thumb-container">
<p><strong>Frukost:</strong> @Model.BreakfastMealName</p> <img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.BreakfastMealName" />
</div> </div>
} }
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.LunchMealName)) { @if (!string.IsNullOrWhiteSpace(Model.LunchMealName)) {
<div class="meal-line"> <div class="meal-line">
@if (Model.LunchThumbnail != null) <p><strong>Lunch:</strong>
{ <a href="/meal/view/@Model.LunchMealId">@Model.LunchMealName</a>
var b64 = Convert.ToBase64String(Model.LunchThumbnail); </p>
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.LunchMealName')"> @if (Model.LunchThumbnail != null)
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.LunchMealName" /> {
</button> var b64 = Convert.ToBase64String(Model.LunchThumbnail);
} <div class="meal-large-thumb-container">
<p><strong>Lunch:</strong> @Model.LunchMealName</p> <img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.LunchMealName" />
</div> </div>
} }
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.DinnerMealName)) {
<div class="meal-line">
<p><strong>Middag:</strong>
<a href="/meal/view/@Model.DinnerMealId">@Model.DinnerMealName</a>
</p>
@if (Model.DinnerThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.DinnerThumbnail);
<div class="meal-large-thumb-container">
<img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.DinnerMealName" />
</div>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.DinnerMealName)) {
<div class="meal-line">
@if (Model.DinnerThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.DinnerThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.DinnerMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.DinnerMealName" />
</button>
}
<p><strong>Middag:</strong> @Model.DinnerMealName</p>
</div>
}
@if (ViewBag.RestaurantIsOpen as bool? == true) @if (ViewBag.RestaurantIsOpen as bool? == true)
{ {

View File

@@ -1,4 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor
@{
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
}
<html lang="sv" ng-app="mealGalleryApp"> <html lang="sv" ng-app="mealGalleryApp">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@@ -6,7 +13,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<link rel="stylesheet" href="/css/meal-gallery.css"> <link rel="stylesheet" href="/css/meal-gallery.css">
<script src="https://kit.fontawesome.com/yourkit.js" crossorigin="anonymous"></script> <!-- om du använder fontawesome --> <script src="https://kit.fontawesome.com/yourkit.js" crossorigin="anonymous"></script>
<style> <style>
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
body { overflow-x: hidden; } body { overflow-x: hidden; }
@@ -16,10 +23,22 @@
<div class="meal-gallery-container"> <div class="meal-gallery-container">
<div class="meal-gallery-header"> <div class="meal-gallery-header">
<h1>Recept</h1> <h1>Recept</h1>
<div class="search-container"> <div class="search-container">
<input type="text" ng-model="search" placeholder="Sök recept..."> <input type="text" ng-model="search" placeholder="Sök recept..." />
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
</div>
@if (isChef)
{
<label class="toggle-published">
<input type="checkbox" ng-model="includeUnpublished" ng-change="reloadMeals()" />
<span>Visa alla</span>
</label>
<a href="/meal/view?edit=true" class="btn-create-meal">+ Ny rätt</a>
}
</div>
</div> </div>
<div class="meal-gallery-grid"> <div class="meal-gallery-grid">
@@ -37,29 +56,34 @@
</div> </div>
<script> <script>
angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) { angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) {
$scope.meals = []; $scope.meals = [];
$scope.search = ""; $scope.search = "";
$scope.visibleCount = 12; $scope.visibleCount = 12;
$scope.includeUnpublished = false;
$http.get("/api/mealMenuApi/getPublishedMeals").then(res => { $scope.reloadMeals = function () {
$scope.meals = res.data; const url = `/api/mealMenuApi/getPublishedMeals?includeUnpublished=${$scope.includeUnpublished}`;
}); $http.get(url).then(res => {
$scope.meals = res.data;
// Lazy loading on scroll
window.addEventListener('scroll', function () {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 150) {
$scope.$applyAsync(() => {
$scope.visibleCount += 8;
});
}
});
// Show all when searching
$scope.$watch('search', function (newVal) {
$scope.visibleCount = newVal ? 9999 : 12;
});
}); });
};
$scope.reloadMeals();
window.addEventListener('scroll', function () {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 150) {
$scope.$applyAsync(() => {
$scope.visibleCount += 8;
});
}
});
$scope.$watch('search', function (newVal) {
$scope.visibleCount = newVal ? 9999 : 12;
});
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -20,6 +20,7 @@
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef"); bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
isEditing = isEditing && isChef; isEditing = isEditing && isChef;
} }
<link rel="stylesheet" href="/css/meal.css">
<div class="meal-container"> <div class="meal-container">
<div class="meal-header"> <div class="meal-header">
@@ -37,6 +38,7 @@
<h1 class="meal-title">@Model.Name</h1> <h1 class="meal-title">@Model.Name</h1>
<p class="description">@Model.Description</p> <p class="description">@Model.Description</p>
</div> </div>
</div> </div>
@if (isEditing) @if (isEditing)
@@ -96,8 +98,13 @@
</div> </div>
} }
</div> </div>
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till ingrediens</button> <div style="margin-top: 0.5rem;">
</div> <label for="bulkIngredients">Klistra in flera ingredienser</label>
<textarea id="bulkIngredients" placeholder="1 dl mjölk&#10;2 tsk socker" class="form-control" rows="3"></textarea>
<button type="button" class="btn-outline" onclick="parseBulkIngredients()">Lägg till från lista</button>
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till rad</button>
</div>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label> <label>
@@ -129,6 +136,18 @@
{ {
<p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Visa Recept</a></p> <p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Visa Recept</a></p>
} }
@if (User.Identity.IsAuthenticated)
{
<div class="rating-box" data-meal-id="@Model.Id">
<p>Ditt betyg:</p>
<div class="star-container">
@for (int i = 1; i <= 5; i++)
{
<i class="fa fa-star" data-value="@i"></i>
}
</div>
</div>
}
<div class="buttons"> <div class="buttons">
@if (isChef) @if (isChef)
@@ -179,11 +198,47 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/ui/trumbowyg.min.css"> <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/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 src="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/trumbowyg.min.js"></script>
<script src="https://kit.fontawesome.com/yourkit.js" crossorigin="anonymous"></script>
<script> <script>
function addIngredientRow() { document.addEventListener("DOMContentLoaded", () => {
const list = document.getElementById('ingredients-list'); const ratingBox = document.querySelector(".rating-box");
if (!ratingBox) return;
const mealId = ratingBox.dataset.mealId;
const stars = ratingBox.querySelectorAll(".fa-star");
fetch(`/api/MealRatingApi/${mealId}`)
.then(res => res.json())
.then(rating => {
stars.forEach(star => {
if (parseInt(star.dataset.value) <= rating) {
star.classList.add("rated");
}
});
});
stars.forEach(star => {
star.addEventListener("click", () => {
const rating = star.dataset.value;
fetch("/api/MealRatingApi", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mealId: mealId, rating: parseInt(rating) })
}).then(() => {
stars.forEach(s => s.classList.remove("rated"));
stars.forEach(s => {
if (parseInt(s.dataset.value) <= rating) {
s.classList.add("rated");
}
});
});
});
});
});
function addIngredientRow(quantity = '', item = '') {
const list = document.getElementById('ingredients-list');
const index = list.children.length; const index = list.children.length;
const div = document.createElement('div'); const div = document.createElement('div');
@@ -194,17 +249,44 @@
qtyInput.name = `Ingredients[${index}].Quantity`; qtyInput.name = `Ingredients[${index}].Quantity`;
qtyInput.placeholder = 'Mängd'; qtyInput.placeholder = 'Mängd';
qtyInput.className = 'form-control ingredient-qty'; qtyInput.className = 'form-control ingredient-qty';
qtyInput.value = quantity;
const itemInput = document.createElement('input'); const itemInput = document.createElement('input');
itemInput.type = 'text'; itemInput.type = 'text';
itemInput.name = `Ingredients[${index}].Item`; itemInput.name = `Ingredients[${index}].Item`;
itemInput.placeholder = 'Ingrediens'; itemInput.placeholder = 'Ingrediens';
itemInput.className = 'form-control ingredient-item'; itemInput.className = 'form-control ingredient-item';
itemInput.value = item;
div.appendChild(qtyInput); div.appendChild(qtyInput);
div.appendChild(itemInput); div.appendChild(itemInput);
list.appendChild(div); list.appendChild(div);
} }
function parseBulkIngredients() {
const bulk = document.getElementById('bulkIngredients').value;
if (!bulk.trim()) return;
const lines = bulk.trim().split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
// Försök dela på första mellanslag → mängd + ingrediens
const parts = trimmed.split(' ');
const quantity = parts.slice(0, 2).join(' '); // typ "2 dl" eller "1 tsk"
const item = parts.slice(2).join(' '); // resten
// fallback om bara ett ord: "ägg"
const safeQuantity = item ? quantity : '';
const safeItem = item || quantity;
addIngredientRow(safeQuantity, safeItem);
}
document.getElementById('bulkIngredients').value = '';
}
function toggleRecipe() { function toggleRecipe() {
const section = document.getElementById('recipe-section'); const section = document.getElementById('recipe-section');
if (!section) return; if (!section) return;
@@ -265,166 +347,6 @@
</script> </script>
<style> <style>
.dragging {
outline: 2px dashed #6a0dad;
}
.meal-image-wrapper {
position: relative;
}
.meal-container {
max-width: 900px;
margin: 2rem auto;
background: #fff;
padding: 2rem;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
font-family: 'Segoe UI', sans-serif;
}
.meal-header {
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: center;
}
.meal-meta {
flex: 1;
}
.meal-title {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
}
.meal-image {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 12px;
border: 1px solid #ccc;
}
.description {
font-size: 1.1rem;
color: #555;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
.meal-details p {
font-size: 1rem;
color: #333;
margin: 0.3rem 0;
}
.label {
font-weight: bold;
color: #6a0dad;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.3rem;
display: block;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
.buttons {
margin-top: 1.5rem;
}
.btn,
.btn-outline {
background-color: #6a0dad;
color: white;
border: none;
padding: 0.6rem 1.4rem;
border-radius: 6px;
margin: 0.4rem;
cursor: pointer;
font-size: 1rem;
text-decoration: none;
}
.btn-outline {
background-color: transparent;
border: 2px solid #6a0dad;
color: #6a0dad;
}
.recipe-link {
color: #6a0dad;
text-decoration: underline;
font-weight: 500;
}
.placeholder {
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%;
}
@@media (max-width: 768px) {
.meal-container {
padding: 1rem;
}
.meal-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.meal-meta {
width: 100%;
}
.meal-title {
font-size: 2rem;
}
.meal-image {
width: 100%;
max-width: 300px;
height: auto;
}
.form-control {
font-size: 1rem;
}
.ingredient-row {
flex-direction: column;
}
.ingredient-qty,
.ingredient-item {
flex: 1 0 auto;
width: 100%;
}
.btn,
.btn-outline {
width: 100%;
margin: 0.25rem 0;
}
.buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
</style> </style>

View File

@@ -46,16 +46,51 @@
margin-bottom: 28px; margin-bottom: 28px;
} }
.meal-lines p { .meal-line {
font-size: 1.2rem; margin-bottom: 0rem;
margin: 8px 0; text-align: center;
color: #0f172a; display: flex;
flex-direction: column;
align-items: center;
} }
.meal-lines strong { .meal-line p {
color: #475569; font-size: 1rem;
margin-bottom: 0.5rem;
}
.meal-line a {
color: #007d36;
text-decoration: none;
font-weight: 500;
}
.meal-line a:hover {
text-decoration: underline;
}
.meal-large-thumb-container {
margin-top: 0rem;
width: 100%;
max-width: 300px;
height: 100px;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
} }
.meal-large-thumb {
width: 100%;
max-width: 300px;
height: 160px;
object-fit: cover;
object-position: center%;
display: block;
border-radius: 8px;
margin-top: 0rem;
}
.no-menu { .no-menu {
font-size: 1.1rem; font-size: 1.1rem;
color: #64748b; color: #64748b;

View File

@@ -24,30 +24,51 @@ body {
margin: 0; margin: 0;
} }
/* Sökfält + checkbox */
.search-container { .search-container {
margin-top: 1rem; margin-top: 1rem;
position: relative; position: relative;
width: 100%; width: 100%;
max-width: 500px; max-width: 600px;
margin-inline: auto; margin-inline: auto;
display: flex;
align-items: center;
gap: 1rem;
background: #e9eef3;
padding: 0.5rem 1rem;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
} }
.search-container input { .search-container input[type="text"] {
width: 100%; flex-grow: 1;
padding: 8px 34px 8px 12px; padding: 8px 12px;
border-radius: 20px; border-radius: 8px;
border: 1px solid #ccc; border: 1px solid #ccc;
font-size: 1rem; font-size: 1rem;
box-sizing: border-box; box-sizing: border-box;
} }
.search-container i { .search-container i {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #888; color: #888;
font-size: 0.9rem; font-size: 1rem;
}
/* Checkboxdel */
.toggle-published {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.9rem;
color: #222;
user-select: none;
white-space: nowrap;
}
.toggle-published input[type="checkbox"] {
accent-color: #3399ff;
transform: scale(1.1);
cursor: pointer;
} }
/* === Grid layout === */ /* === Grid layout === */
@@ -120,22 +141,29 @@ body {
/* === Mobilanpassning === */ /* === Mobilanpassning === */
@media (max-width: 600px) { @media (max-width: 600px) {
.meal-gallery-grid { .meal-gallery-grid {
grid-template-columns: repeat(2, 1fr); /* exakt 2 per rad */ grid-template-columns: repeat(2, 1fr);
} }
.meal-gallery-header h1 { .meal-gallery-header h1 {
font-size: 1.6rem; font-size: 1.6rem;
} }
.search-container { .search-container {
flex-direction: column;
align-items: stretch;
padding: 1rem;
gap: 0.75rem;
max-width: 90%; max-width: 90%;
} }
.search-container input { .search-container input[type="text"] {
padding: 6px 30px 6px 10px; font-size: 0.95rem;
font-size: 0.9rem;
border-radius: 18px;
} }
.toggle-published {
justify-content: center;
}
.meal-card img { .meal-card img {
height: 130px; height: 130px;
} }
@@ -157,3 +185,29 @@ body {
font-size: 0.85rem; font-size: 0.85rem;
} }
} }
.btn-create-meal {
background-color: #3399ff;
color: white;
padding: 8px 14px;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
border: none;
cursor: pointer;
transition: background-color 0.2s ease;
white-space: nowrap;
}
.btn-create-meal:hover {
background-color: #2389e0;
}
@media (max-width: 600px) {
.btn-create-meal {
width: 100%;
text-align: center;
}
}

View File

@@ -0,0 +1,199 @@
.admin-actions a.btn, .admin-actions a.btn-outline {
font-size: 1rem;
padding: 0.5rem 1.2rem;
}
.dragging {
outline: 2px dashed #6a0dad;
}
.meal-image-wrapper {
position: relative;
}
.meal-container {
max-width: 900px;
margin: 2rem auto;
background: #fff;
padding: 2rem;
border-radius: 16px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
font-family: 'Segoe UI', sans-serif;
}
.meal-header {
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: center;
}
.meal-meta {
flex: 1;
}
.meal-title {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
}
.meal-image {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 12px;
border: 1px solid #ccc;
}
.description {
font-size: 1.1rem;
color: #555;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
}
.meal-details p {
font-size: 1rem;
color: #333;
margin: 0.3rem 0;
}
.label {
font-weight: bold;
color: #6a0dad;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.3rem;
display: block;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
.buttons {
margin-top: 1.5rem;
}
.btn,
.btn-outline {
background-color: #6a0dad;
color: white;
border: none;
padding: 0.6rem 1.4rem;
border-radius: 6px;
margin: 0.4rem;
cursor: pointer;
font-size: 1rem;
text-decoration: none;
}
.btn-outline {
background-color: transparent;
border: 2px solid #6a0dad;
color: #6a0dad;
}
.recipe-link {
color: #6a0dad;
text-decoration: underline;
font-weight: 500;
}
.placeholder {
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%;
}
@@media (max-width: 768px) {
.meal-container {
padding: 1rem;
}
.meal-header {
flex-direction: column;
align-items: center;
text-align: center;
}
.meal-meta {
width: 100%;
}
.meal-title {
font-size: 2rem;
}
.meal-image {
width: 100%;
max-width: 300px;
height: auto;
}
.form-control {
font-size: 1rem;
}
.ingredient-row {
flex-direction: column;
}
.ingredient-qty,
.ingredient-item {
flex: 1 0 auto;
width: 100%;
}
.btn,
.btn-outline {
width: 100%;
margin: 0.25rem 0;
}
.buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}
.star-container {
display: inline-flex;
gap: 5px;
cursor: pointer;
}
.fa-star {
font-size: 1.5rem;
color: #ccc;
transition: color 0.2s ease;
}
.fa-star.rated {
color: #ffcc00;
}

View File

@@ -429,3 +429,39 @@ body {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
} }
.btn-lewel {
background-color: #3399ff; /* blå accent */
color: white;
border: none;
padding: 0.5rem 1.2rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
display: inline-block;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-lewel:hover {
background-color: #2389e0;
}
.btn-lewel-outline {
background-color: transparent;
color: #3399ff;
border: 2px solid #3399ff;
padding: 0.5rem 1.2rem;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
display: inline-block;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.btn-lewel-outline:hover {
background-color: #e0f0ff;
color: #1F2C3C;
}