webapp: .NET 9 Razor Pages, neue rezepte, systemd service
This commit is contained in:
1 parent
07428b0456
commit
466cb09d1c
20 files changed
+969
-4
No files matched your search
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kochstube.Models;
|
||||
|
||||
public class Rezept
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("titel")]
|
||||
public string Titel { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("beschreibung")]
|
||||
public string Beschreibung { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("bild")]
|
||||
public string? Bild { get; set; }
|
||||
|
||||
[JsonPropertyName("kategorie")]
|
||||
public string Kategorie { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public List<string> Tags { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("schwierigkeit")]
|
||||
public string Schwierigkeit { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("zubereitungszeit_min")]
|
||||
public int ZubereitungszeitMin { get; set; }
|
||||
|
||||
[JsonPropertyName("portionen")]
|
||||
public int Portionen { get; set; }
|
||||
|
||||
[JsonPropertyName("zutaten")]
|
||||
public List<Zutat> Zutaten { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("zubereitung")]
|
||||
public List<string> Zubereitung { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("notizen")]
|
||||
public string? Notizen { get; set; }
|
||||
|
||||
[JsonPropertyName("quelle")]
|
||||
public Quelle? Quelle { get; set; }
|
||||
|
||||
[JsonPropertyName("erstellt")]
|
||||
public string? Erstellt { get; set; }
|
||||
}
|
||||
|
||||
public class Zutat
|
||||
{
|
||||
[JsonPropertyName("menge")]
|
||||
public string Menge { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("zutat")]
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
public class Quelle
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonPropertyName("autor")]
|
||||
public string? Autor { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
@page
|
||||
@model IndexModel
|
||||
|
||||
@section FilterBar {
|
||||
<div class="search-bar-wrap">
|
||||
<form method="get" class="search-bar">
|
||||
<input type="text" name="q" value="@Model.Q" placeholder="🔍 Rezept suchen..." />
|
||||
<select name="tag">
|
||||
<option value="">Alle Tags</option>
|
||||
@foreach (var t in Model.AlleTags)
|
||||
{
|
||||
<option value="@t" selected="@(Model.Tag == t)">@t</option>
|
||||
}
|
||||
</select>
|
||||
<select name="kategorie">
|
||||
<option value="">Alle Kategorien</option>
|
||||
@foreach (var k in Model.AlleKategorien)
|
||||
{
|
||||
<option value="@k" selected="@(Model.Kategorie == k)">@k</option>
|
||||
}
|
||||
</select>
|
||||
<button type="submit">Suchen</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="stats">@Model.Rezepte.Count Rezept(e)</div>
|
||||
|
||||
<div class="grid">
|
||||
@foreach (var r in Model.Rezepte)
|
||||
{
|
||||
<a class="card" href="/rezept/@r.Id">
|
||||
@if (r.ZubereitungszeitMin <= 15)
|
||||
{
|
||||
<div class="badge-quick">⚡ @r.ZubereitungszeitMin Min</div>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(r.Bild))
|
||||
{
|
||||
<img class="card-img" src="@r.Bild" alt="@r.Titel" loading="lazy" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card-img-placeholder">🍽️</div>
|
||||
}
|
||||
<div class="card-body">
|
||||
<h3>@r.Titel</h3>
|
||||
<p>@(r.Beschreibung.Length > 120 ? r.Beschreibung[..120] + "..." : r.Beschreibung)</p>
|
||||
<div class="card-meta">
|
||||
<span>⏱ @r.ZubereitungszeitMin Min</span>
|
||||
<span>👥 @r.Portionen Port.</span>
|
||||
<span>📊 @r.Schwierigkeit</span>
|
||||
</div>
|
||||
<div class="tags">
|
||||
@foreach (var tag in r.Tags)
|
||||
{
|
||||
<span class="tag">@tag</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Kochstube.Models;
|
||||
using Kochstube.Services;
|
||||
|
||||
namespace Kochstube.Pages;
|
||||
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
private readonly RezeptService _service;
|
||||
|
||||
public IndexModel(RezeptService service) => _service = service;
|
||||
|
||||
public List<Rezept> Rezepte { get; set; } = [];
|
||||
public List<string> AlleTags { get; set; } = [];
|
||||
public List<string> AlleKategorien { get; set; } = [];
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? Q { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? Tag { get; set; }
|
||||
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public string? Kategorie { get; set; }
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
AlleTags = _service.GetAllTags();
|
||||
AlleKategorien = _service.GetAllKategorien();
|
||||
Rezepte = _service.Search(Q, Tag, Kategorie);
|
||||
ViewData["Title"] = "Rezepte";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
@page "/rezept/{id}"
|
||||
@model RezeptModel
|
||||
|
||||
@if (Model.Rezept == null) { return; }
|
||||
|
||||
@{ var r = Model.Rezept; }
|
||||
|
||||
<div class="detail">
|
||||
<a class="back-link" href="/">← Zurück zur Übersicht</a>
|
||||
|
||||
<h1>@r.Titel</h1>
|
||||
<p class="desc">@r.Beschreibung</p>
|
||||
|
||||
@if (!string.IsNullOrEmpty(r.Bild))
|
||||
{
|
||||
<img class="detail-img" src="@r.Bild" alt="@r.Titel" />
|
||||
}
|
||||
|
||||
<div class="detail-meta">
|
||||
<div class="meta-item">
|
||||
<span class="label">Zubereitungszeit</span>
|
||||
<span class="value">@r.ZubereitungszeitMin min</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Portionen</span>
|
||||
<span class="value">@r.Portionen</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Schwierigkeit</span>
|
||||
<span class="value">@r.Schwierigkeit</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="label">Kategorie</span>
|
||||
<span class="value">@r.Kategorie</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tags" style="margin-bottom: 1.5rem;">
|
||||
@foreach (var tag in r.Tags)
|
||||
{
|
||||
<a class="tag" href="/?tag=@tag">@tag</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Zutaten</h2>
|
||||
<ul class="zutaten-list">
|
||||
@foreach (var z in r.Zutaten)
|
||||
{
|
||||
<li>
|
||||
<span>@z.Name</span>
|
||||
<span class="menge">@z.Menge</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Zubereitung</h2>
|
||||
<ol class="steps">
|
||||
@foreach (var step in r.Zubereitung)
|
||||
{
|
||||
<li>@step</li>
|
||||
}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(r.Notizen))
|
||||
{
|
||||
<div class="section">
|
||||
<h2>Notizen</h2>
|
||||
<div class="notizen">@r.Notizen</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (r.Quelle != null)
|
||||
{
|
||||
<div class="section quelle">
|
||||
Quelle: <a href="@r.Quelle.Url" target="_blank">@r.Quelle.Name</a>
|
||||
@if (!string.IsNullOrEmpty(r.Quelle.Autor))
|
||||
{
|
||||
<span> von @r.Quelle.Autor</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Kochstube.Models;
|
||||
using Kochstube.Services;
|
||||
|
||||
namespace Kochstube.Pages;
|
||||
|
||||
public class RezeptModel : PageModel
|
||||
{
|
||||
private readonly RezeptService _service;
|
||||
|
||||
public RezeptModel(RezeptService service) => _service = service;
|
||||
|
||||
public Rezept? Rezept { get; set; }
|
||||
|
||||
public IActionResult OnGet(string id)
|
||||
{
|
||||
Rezept = _service.GetById(id);
|
||||
if (Rezept == null) return NotFound();
|
||||
ViewData["Title"] = Rezept.Titel;
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>@ViewData["Title"] - Kochstube</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f0e8;
|
||||
--card: #ffffff;
|
||||
--accent: #d94f30;
|
||||
--accent2: #fef3e2;
|
||||
--text: #2d2a26;
|
||||
--muted: #7a7470;
|
||||
--tag-bg: #f0e6d6;
|
||||
--tag-text: #6b5e4f;
|
||||
--quick: #e8960c;
|
||||
--border: #e8e0d4;
|
||||
--shadow: rgba(120, 100, 70, .1);
|
||||
--radius: 16px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem 1.5rem;
|
||||
background: linear-gradient(135deg, #fff8ef, #fef3e2);
|
||||
border-bottom: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
header h1 { font-size: 2.4rem; letter-spacing: 2px; color: var(--text); }
|
||||
header h1 span { color: var(--accent); }
|
||||
header p { color: var(--muted); margin-top: .4rem; font-size: .95rem; }
|
||||
header a { text-decoration: none; color: inherit; }
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
/* Search / Filter bar */
|
||||
.search-bar-wrap {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
justify-content: center;
|
||||
padding: 1.2rem 1rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.search-bar input, .search-bar select {
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
padding: .45rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: .85rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-bar input { flex: 1; min-width: 200px; }
|
||||
.search-bar input::placeholder { color: #b5ada5; }
|
||||
.search-bar input:focus, .search-bar select:focus { border-color: var(--accent); }
|
||||
|
||||
.search-bar select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: .45rem 1.2rem;
|
||||
border-radius: 20px;
|
||||
font-size: .85rem;
|
||||
cursor: pointer;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.search-bar button:hover { opacity: .9; }
|
||||
|
||||
.stats {
|
||||
color: var(--muted);
|
||||
margin-bottom: 1rem;
|
||||
font-size: .9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
transition: transform .2s, box-shadow .2s;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
position: relative;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 2px 12px var(--shadow);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(120, 100, 70, .18);
|
||||
}
|
||||
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-img-placeholder {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: var(--tag-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.badge-quick {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: var(--quick);
|
||||
color: #fff;
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
padding: .25rem .6rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.15);
|
||||
}
|
||||
|
||||
.card-body { padding: 1rem 1.2rem 1.2rem; }
|
||||
.card-body h3 { font-size: 1.1rem; margin-bottom: .5rem; line-height: 1.3; }
|
||||
.card-body p {
|
||||
color: var(--muted);
|
||||
font-size: .85rem;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
gap: .8rem;
|
||||
margin-top: .7rem;
|
||||
font-size: .8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.card-meta span { display: flex; align-items: center; gap: .3rem; }
|
||||
|
||||
.tags { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .6rem; }
|
||||
|
||||
.tag {
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-text);
|
||||
padding: .15rem .55rem;
|
||||
border-radius: 8px;
|
||||
font-size: .72rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag:hover { opacity: .8; }
|
||||
|
||||
/* Detail page */
|
||||
.detail { max-width: 750px; margin: 0 auto; }
|
||||
.detail h1 { font-size: 1.8rem; margin-bottom: .3rem; }
|
||||
.detail .desc { color: var(--muted); font-size: 1.05rem; margin-bottom: 1.2rem; line-height: 1.5; }
|
||||
|
||||
.detail-img {
|
||||
width: 100%;
|
||||
max-height: 350px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 4px 20px var(--shadow);
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
background: var(--accent2);
|
||||
padding: .5rem 1rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--border);
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.meta-item .label { font-size: .7rem; color: var(--muted); display: block; text-transform: uppercase; letter-spacing: 1px; }
|
||||
.meta-item .value { font-size: 1.1rem; font-weight: 700; }
|
||||
|
||||
.section { margin-bottom: 2rem; }
|
||||
.section h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: .6rem;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.zutaten-list { list-style: none; }
|
||||
.zutaten-list li {
|
||||
padding: .5rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .92rem;
|
||||
}
|
||||
.zutaten-list .menge { color: var(--accent); font-weight: 600; }
|
||||
|
||||
.steps { list-style: none; counter-reset: step; }
|
||||
.steps li {
|
||||
counter-increment: step;
|
||||
padding: .8rem 0;
|
||||
padding-left: 2.5rem;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--border);
|
||||
line-height: 1.5;
|
||||
font-size: .92rem;
|
||||
}
|
||||
.steps li::before {
|
||||
content: counter(step);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notizen {
|
||||
background: #fef6e9;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border-left: 3px solid var(--quick);
|
||||
color: var(--muted);
|
||||
font-size: .9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.quelle {
|
||||
font-size: .85rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
.quelle a { color: var(--accent); text-decoration: none; }
|
||||
.quelle a:hover { text-decoration: underline; }
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.back-link:hover { text-decoration: underline; }
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@@media (max-width: 600px) {
|
||||
header { padding: 1.5rem 1rem 1rem; }
|
||||
header h1 { font-size: 1.8rem; }
|
||||
main { padding: 1rem 0.5rem; }
|
||||
.grid { grid-template-columns: 1fr; gap: 1rem; }
|
||||
.detail-body { padding: 1.2rem; }
|
||||
.search-bar input { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="/">
|
||||
<h1>🍳 Koch<span>stube</span></h1>
|
||||
<p>Rezepte von Tausi & Nocki</p>
|
||||
</a>
|
||||
</header>
|
||||
@RenderSection("FilterBar", required: false)
|
||||
<main>
|
||||
@RenderBody()
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
@using Kochstube.Models
|
||||
@namespace Kochstube.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "/Pages/Shared/_Layout.cshtml";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Kochstube.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddSingleton<RezeptService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.MapRazorPages();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5134",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Text.Json;
|
||||
using Kochstube.Models;
|
||||
|
||||
namespace Kochstube.Services;
|
||||
|
||||
public class RezeptService
|
||||
{
|
||||
private readonly string _rezeptPath;
|
||||
private List<Rezept>? _cache;
|
||||
private DateTime _lastLoad;
|
||||
|
||||
public RezeptService(IConfiguration config)
|
||||
{
|
||||
_rezeptPath = config["RezeptPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "rezepte");
|
||||
}
|
||||
|
||||
public List<Rezept> GetAll()
|
||||
{
|
||||
// Reload if older than 10 seconds (good enough for dev)
|
||||
if (_cache == null || DateTime.UtcNow - _lastLoad > TimeSpan.FromSeconds(10))
|
||||
{
|
||||
_cache = LoadFromDisk();
|
||||
_lastLoad = DateTime.UtcNow;
|
||||
}
|
||||
return _cache;
|
||||
}
|
||||
|
||||
public Rezept? GetById(string id) => GetAll().FirstOrDefault(r => r.Id == id);
|
||||
|
||||
public List<string> GetAllTags() => GetAll().SelectMany(r => r.Tags).Distinct().OrderBy(t => t).ToList();
|
||||
|
||||
public List<string> GetAllKategorien() => GetAll().Select(r => r.Kategorie).Distinct().OrderBy(k => k).ToList();
|
||||
|
||||
public List<Rezept> Search(string? query, string? tag, string? kategorie)
|
||||
{
|
||||
var results = GetAll().AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var q = query.ToLowerInvariant();
|
||||
results = results.Where(r =>
|
||||
r.Titel.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
r.Beschreibung.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
r.Zutaten.Any(z => z.Name.Contains(q, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tag))
|
||||
results = results.Where(r => r.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(kategorie))
|
||||
results = results.Where(r => r.Kategorie.Equals(kategorie, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
private List<Rezept> LoadFromDisk()
|
||||
{
|
||||
var dir = Path.GetFullPath(_rezeptPath);
|
||||
if (!Directory.Exists(dir)) return [];
|
||||
|
||||
var rezepte = new List<Rezept>();
|
||||
foreach (var file in Directory.GetFiles(dir, "*.json"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var r = JsonSerializer.Deserialize<Rezept>(json);
|
||||
if (r != null) rezepte.Add(r);
|
||||
}
|
||||
catch { /* skip broken files */ }
|
||||
}
|
||||
return rezepte.OrderByDescending(r => r.Erstellt ?? "").ThenBy(r => r.Titel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"RezeptPath": "/var/lib/docker/workspace/stacks/openclaw/meleki/home/kochstube/rezepte",
|
||||
"Urls": "http://0.0.0.0:18683",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user