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")]
public IActionResult GetPublishedMeals()
public IActionResult GetPublishedMeals([FromQuery] bool includeUnpublished = false)
{
var meals = _menuService.GetMeals()
.Where(m => m.IsPublished) // 🟢 filtrera här!
.Where(m => includeUnpublished || m.IsPublished)
.Select(m => new {
m.Id,
m.Name,
@@ -44,6 +44,7 @@ namespace Aberwyn.Controllers
}
[HttpGet("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<LabIngredient> LabIngredients { 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 =>
{
b.Property<int>("Id")
@@ -868,6 +894,17 @@ namespace Aberwyn.Migrations
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 =>
{
b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder")

View File

@@ -177,5 +177,22 @@ public class WeeklyMenu
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">
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) {
<div class="meal-line">
@if (Model.BreakfastThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.BreakfastThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.BreakfastMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.BreakfastMealName" />
</button>
}
<p><strong>Frukost:</strong> @Model.BreakfastMealName</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) {
<div class="meal-line">
<p><strong>Frukost:</strong>
<a href="/meal/view/@Model.BreakfastMealId">@Model.BreakfastMealName</a>
</p>
@if (Model.BreakfastThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.BreakfastThumbnail);
<div class="meal-large-thumb-container">
<img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.BreakfastMealName" />
</div>
}
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.LunchMealName)) {
<div class="meal-line">
@if (Model.LunchThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.LunchThumbnail);
<button type="button" class="thumb-button" onclick="showLargeImage('@b64', '@Model.LunchMealName')">
<img class="meal-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.LunchMealName" />
</button>
}
<p><strong>Lunch:</strong> @Model.LunchMealName</p>
</div>
}
@if (!string.IsNullOrWhiteSpace(Model.LunchMealName)) {
<div class="meal-line">
<p><strong>Lunch:</strong>
<a href="/meal/view/@Model.LunchMealId">@Model.LunchMealName</a>
</p>
@if (Model.LunchThumbnail != null)
{
var b64 = Convert.ToBase64String(Model.LunchThumbnail);
<div class="meal-large-thumb-container">
<img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.LunchMealName" />
</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)
{

View File

@@ -1,4 +1,11 @@
<!DOCTYPE html>
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor
@{
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
}
<html lang="sv" ng-app="mealGalleryApp">
<head>
<meta charset="utf-8">
@@ -6,7 +13,7 @@
<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>
<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>
*, *::before, *::after { box-sizing: border-box; }
body { overflow-x: hidden; }
@@ -16,10 +23,22 @@
<div class="meal-gallery-container">
<div class="meal-gallery-header">
<h1>Recept</h1>
<div class="search-container">
<input type="text" ng-model="search" placeholder="Sök recept...">
<i class="fa-solid fa-magnifying-glass"></i>
</div>
<div class="search-container">
<input type="text" ng-model="search" placeholder="Sök recept..." />
<i class="fa-solid fa-magnifying-glass"></i>
@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 class="meal-gallery-grid">
@@ -37,29 +56,34 @@
</div>
<script>
angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) {
$scope.meals = [];
$scope.search = "";
$scope.visibleCount = 12;
angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) {
$scope.meals = [];
$scope.search = "";
$scope.visibleCount = 12;
$scope.includeUnpublished = false;
$http.get("/api/mealMenuApi/getPublishedMeals").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 = function () {
const url = `/api/mealMenuApi/getPublishedMeals?includeUnpublished=${$scope.includeUnpublished}`;
$http.get(url).then(res => {
$scope.meals = res.data;
});
};
$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>
</body>
</html>

View File

@@ -20,6 +20,7 @@
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
isEditing = isEditing && isChef;
}
<link rel="stylesheet" href="/css/meal.css">
<div class="meal-container">
<div class="meal-header">
@@ -37,6 +38,7 @@
<h1 class="meal-title">@Model.Name</h1>
<p class="description">@Model.Description</p>
</div>
</div>
@if (isEditing)
@@ -96,8 +98,13 @@
</div>
}
</div>
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till ingrediens</button>
</div>
<div style="margin-top: 0.5rem;">
<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 class="form-group">
<label>
@@ -129,6 +136,18 @@
{
<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">
@if (isChef)
@@ -179,11 +198,47 @@
<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 src="https://kit.fontawesome.com/yourkit.js" crossorigin="anonymous"></script>
<script>
function addIngredientRow() {
const list = document.getElementById('ingredients-list');
document.addEventListener("DOMContentLoaded", () => {
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 div = document.createElement('div');
@@ -194,17 +249,44 @@
qtyInput.name = `Ingredients[${index}].Quantity`;
qtyInput.placeholder = 'Mängd';
qtyInput.className = 'form-control ingredient-qty';
qtyInput.value = quantity;
const itemInput = document.createElement('input');
itemInput.type = 'text';
itemInput.name = `Ingredients[${index}].Item`;
itemInput.placeholder = 'Ingrediens';
itemInput.className = 'form-control ingredient-item';
itemInput.value = item;
div.appendChild(qtyInput);
div.appendChild(itemInput);
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() {
const section = document.getElementById('recipe-section');
if (!section) return;
@@ -265,166 +347,6 @@
</script>
<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>

View File

@@ -46,16 +46,51 @@
margin-bottom: 28px;
}
.meal-lines p {
font-size: 1.2rem;
margin: 8px 0;
color: #0f172a;
.meal-line {
margin-bottom: 0rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
}
.meal-lines strong {
color: #475569;
.meal-line p {
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 {
font-size: 1.1rem;
color: #64748b;

View File

@@ -24,30 +24,51 @@ body {
margin: 0;
}
/* Sökfält + checkbox */
.search-container {
margin-top: 1rem;
position: relative;
width: 100%;
max-width: 500px;
max-width: 600px;
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 {
width: 100%;
padding: 8px 34px 8px 12px;
border-radius: 20px;
.search-container input[type="text"] {
flex-grow: 1;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid #ccc;
font-size: 1rem;
box-sizing: border-box;
}
.search-container i {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
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 === */
@@ -120,22 +141,29 @@ body {
/* === Mobilanpassning === */
@media (max-width: 600px) {
.meal-gallery-grid {
grid-template-columns: repeat(2, 1fr); /* exakt 2 per rad */
grid-template-columns: repeat(2, 1fr);
}
.meal-gallery-header h1 {
font-size: 1.6rem;
}
.search-container {
flex-direction: column;
align-items: stretch;
padding: 1rem;
gap: 0.75rem;
max-width: 90%;
}
.search-container input {
padding: 6px 30px 6px 10px;
font-size: 0.9rem;
border-radius: 18px;
.search-container input[type="text"] {
font-size: 0.95rem;
}
.toggle-published {
justify-content: center;
}
.meal-card img {
height: 130px;
}
@@ -157,3 +185,29 @@ body {
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;
}
}
.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;
}