- Entities: Site (singleton, navOrder JSON), Page (slug/title/seoDescription/status enum),
Block (single table, Type enum + Data JSON; RichText=Markdown), Media. Migration
AddCmsEntities, seeds 6 Jimdo-mirror pages (placeholder Heading each; abgabetiere also an
auto AbgabetiereList block) + Site singleton nav.
- GET /api/site-snapshot: one JSON doc (site nav + pages + ordered blocks {id,order,type,data});
AbgabetiereList(auto) resolved from live ForSale gerbils -> {name, farbschlag, group(=Becken),
photos[urls], aiSaleText:null} (no price per god). SiteSnapshotService.
- CRUD: GET/POST/PUT/DELETE pages; add/update/delete blocks; PUT .../blocks/order reorder.
Block type allowlisted by enum; data validated as JSON object.
125 lines
4.9 KiB
C#
125 lines
4.9 KiB
C#
using System.Text.Json.Nodes;
|
|
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GerbilManagerWebAPI.Cms
|
|
{
|
|
/// <summary>
|
|
/// Builds the full CMS snapshot consumed by the static site renderer (WEB-1). The
|
|
/// abgabetiere page's auto AbgabetiereList block is resolved here from live ForSale
|
|
/// animals — nothing private is ever exposed (publish = snapshot of public content).
|
|
/// </summary>
|
|
public sealed class SiteSnapshotService
|
|
{
|
|
private readonly ApplicationContext _db;
|
|
|
|
public SiteSnapshotService(ApplicationContext db) => _db = db;
|
|
|
|
public async Task<JsonObject> BuildAsync()
|
|
{
|
|
var site = await _db.Sites.AsNoTracking().FirstOrDefaultAsync()
|
|
?? new Site { Id = Site.SingletonId };
|
|
var pages = await _db.Pages.AsNoTracking().Include(p => p.Blocks).ToListAsync();
|
|
var pageById = pages.ToDictionary(p => p.Id);
|
|
|
|
var navOrder = new JsonArray();
|
|
foreach (var id in site.NavOrder)
|
|
if (pageById.TryGetValue(id, out var p))
|
|
navOrder.Add(p.Slug);
|
|
|
|
JsonArray? forSale = null; // resolved lazily, reused across blocks
|
|
|
|
int NavIndex(Guid id) { var i = site.NavOrder.IndexOf(id); return i < 0 ? int.MaxValue : i; }
|
|
|
|
var pagesArr = new JsonArray();
|
|
foreach (var page in pages.OrderBy(p => NavIndex(p.Id)).ThenBy(p => p.Slug))
|
|
{
|
|
var blocks = new JsonArray();
|
|
foreach (var b in page.Blocks.OrderBy(b => b.Order))
|
|
{
|
|
var data = JsonNode.Parse(string.IsNullOrWhiteSpace(b.Data) ? "{}" : b.Data) as JsonObject
|
|
?? new JsonObject();
|
|
if (b.Type == BlockType.AbgabetiereList && AsString(data["mode"]) == "auto")
|
|
{
|
|
forSale ??= await BuildForSaleAsync();
|
|
data["animals"] = forSale.DeepClone();
|
|
}
|
|
blocks.Add(new JsonObject
|
|
{
|
|
["id"] = b.Id.ToString(),
|
|
["order"] = b.Order,
|
|
["type"] = b.Type.ToString(),
|
|
["data"] = data,
|
|
});
|
|
}
|
|
pagesArr.Add(new JsonObject
|
|
{
|
|
["slug"] = page.Slug,
|
|
["title"] = page.Title,
|
|
["seoDescription"] = page.SeoDescription,
|
|
["status"] = page.Status.ToString(),
|
|
["blocks"] = blocks,
|
|
});
|
|
}
|
|
|
|
return new JsonObject
|
|
{
|
|
["site"] = new JsonObject
|
|
{
|
|
["defaultLocale"] = site.DefaultLocale,
|
|
["navOrder"] = navOrder,
|
|
},
|
|
["pages"] = pagesArr,
|
|
};
|
|
}
|
|
|
|
private async Task<JsonArray> BuildForSaleAsync()
|
|
{
|
|
var animals = await _db.Gerbils.AsNoTracking()
|
|
.Where(g => g.Status == GerbilStatus.ForSale)
|
|
.OrderBy(g => g.Name)
|
|
.Select(g => new
|
|
{
|
|
g.Id,
|
|
g.Name,
|
|
Farbschlag = g.ColorVariety != null ? g.ColorVariety.Name : null,
|
|
// group = Becken (enclosure) name — matches how the Abgabe composer
|
|
// groups ForSale animals by enclosure-mates (god ruling). null = single.
|
|
Group = g.Enclosure != null ? g.Enclosure.Name : null,
|
|
})
|
|
.ToListAsync();
|
|
|
|
var ids = animals.Select(a => a.Id).ToList();
|
|
var photos = await _db.GerbilPhotos.AsNoTracking()
|
|
.Where(p => ids.Contains(p.GerbilId))
|
|
.OrderBy(p => p.SortOrder)
|
|
.Select(p => new { p.GerbilId, p.FileName })
|
|
.ToListAsync();
|
|
var photosByGerbil = photos
|
|
.GroupBy(p => p.GerbilId)
|
|
.ToDictionary(g => g.Key, g => g.Select(x => $"/photos/files/{x.FileName}").ToList());
|
|
|
|
var arr = new JsonArray();
|
|
foreach (var a in animals)
|
|
{
|
|
var photoArr = new JsonArray();
|
|
if (photosByGerbil.TryGetValue(a.Id, out var urls))
|
|
foreach (var u in urls) photoArr.Add(u);
|
|
|
|
arr.Add(new JsonObject
|
|
{
|
|
["name"] = a.Name,
|
|
["farbschlag"] = a.Farbschlag,
|
|
["group"] = a.Group,
|
|
["photos"] = photoArr,
|
|
["aiSaleText"] = null, // populated by WEB-5 once a Gemini key exists
|
|
});
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
private static string? AsString(JsonNode? node) =>
|
|
node is JsonValue v && v.TryGetValue<string>(out var s) ? s : null;
|
|
}
|
|
}
|