Meal rating and some fixes to meals
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:
@@ -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()
|
||||
{
|
||||
|
||||
79
Aberwyn/Controllers/MealRatingApiController.cs
Normal file
79
Aberwyn/Controllers/MealRatingApiController.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
1060
Aberwyn/Migrations/20250707125951_AddMealRating.Designer.cs
generated
Normal file
1060
Aberwyn/Migrations/20250707125951_AddMealRating.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
Aberwyn/Migrations/20250707125951_AddMealRating.cs
Normal file
49
Aberwyn/Migrations/20250707125951_AddMealRating.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,44 +19,51 @@
|
||||
{
|
||||
<div class="meal-lines">
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.BreakfastMealName)) {
|
||||
@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);
|
||||
<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 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)) {
|
||||
@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);
|
||||
<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 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)) {
|
||||
@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);
|
||||
<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 class="meal-large-thumb-container">
|
||||
<img class="meal-large-thumb" src="data:image/jpeg;base64,@b64" alt="@Model.DinnerMealName" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@if (ViewBag.RestaurantIsOpen as bool? == true)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
@@ -17,9 +24,21 @@
|
||||
<div class="meal-gallery-header">
|
||||
<h1>Recept</h1>
|
||||
<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>
|
||||
|
||||
@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,16 +56,21 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
angular.module("mealGalleryApp", []).controller("MealGalleryController", function ($scope, $http) {
|
||||
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.reloadMeals = function () {
|
||||
const url = `/api/mealMenuApi/getPublishedMeals?includeUnpublished=${$scope.includeUnpublished}`;
|
||||
$http.get(url).then(res => {
|
||||
$scope.meals = res.data;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reloadMeals();
|
||||
|
||||
// Lazy loading on scroll
|
||||
window.addEventListener('scroll', function () {
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 150) {
|
||||
$scope.$applyAsync(() => {
|
||||
@@ -55,11 +79,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Show all when searching
|
||||
$scope.$watch('search', function (newVal) {
|
||||
$scope.visibleCount = newVal ? 9999 : 12;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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,7 +98,12 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="btn-outline" onclick="addIngredientRow()">+ Lägg till ingrediens</button>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
<label for="bulkIngredients">Klistra in flera ingredienser</label>
|
||||
<textarea id="bulkIngredients" placeholder="1 dl mjölk 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">
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: 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,20 +141,27 @@ 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 {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
199
Aberwyn/wwwroot/css/meal.css
Normal file
199
Aberwyn/wwwroot/css/meal.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user