f2d6300d72
deploy / deploy (push) Failing after 13s
.gitignore has '/data' which Windows git (case-insensitive) silently matched '/Data/', so AppDbContext.cs was never committed and the Docker build (Linux, case-sensitive) failed with CS0234 'Data' not found. Renaming the directory to 'Database/' sidesteps the collision. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.6 KiB
C#
108 lines
3.6 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using SoroushAsadi.Database;
|
|
|
|
namespace SoroushAsadi.Services;
|
|
|
|
/// <summary>
|
|
/// Merges hardcoded default content with admin overrides stored in SQLite.
|
|
/// Sections are keyed by name ("hero", "services", etc.).
|
|
/// The stored JSON is {"fa": {...}, "en": {...}} for bilingual sections,
|
|
/// or a slug-keyed map for "posts".
|
|
/// </summary>
|
|
public class ContentService(AppDbContext db)
|
|
{
|
|
private static readonly JsonSerializerOptions _json =
|
|
new() { PropertyNameCaseInsensitive = true };
|
|
|
|
// ── Public API ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Returns the merged content for a section as a JsonNode.
|
|
/// Callers get the locale-specific sub-object (e.g. node["en"]).</summary>
|
|
public JsonNode? GetSection(string key)
|
|
{
|
|
try
|
|
{
|
|
var row = db.ContentSections.Find(key);
|
|
if (row is null) return null;
|
|
return JsonNode.Parse(row.DataJson);
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
/// <summary>Returns merged bilingual content for the given section + locale.</summary>
|
|
public JsonNode? GetSectionLocale(string key, string locale)
|
|
{
|
|
var node = GetSection(key);
|
|
return node?[locale];
|
|
}
|
|
|
|
/// <summary>Returns all section rows (for admin listing).</summary>
|
|
public IReadOnlyList<string> GetSectionKeys() =>
|
|
db.ContentSections.Select(s => s.Key).ToList();
|
|
|
|
/// <summary>Upserts a section's JSON data.</summary>
|
|
public void SaveSection(string key, string json)
|
|
{
|
|
var row = db.ContentSections.Find(key);
|
|
if (row is null)
|
|
{
|
|
row = new Models.ContentSection { Key = key };
|
|
db.ContentSections.Add(row);
|
|
}
|
|
row.DataJson = json;
|
|
row.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
db.SaveChanges();
|
|
}
|
|
|
|
/// <summary>Deletes a section override (restores built-in default).</summary>
|
|
public void DeleteSection(string key)
|
|
{
|
|
var row = db.ContentSections.Find(key);
|
|
if (row is not null)
|
|
{
|
|
db.ContentSections.Remove(row);
|
|
db.SaveChanges();
|
|
}
|
|
}
|
|
|
|
// ── Posts (stored under key "posts") ─────────────────────────────────
|
|
|
|
public const string PostsKey = "posts";
|
|
|
|
/// <summary>Returns the slug→PostContent map from DB, or empty.</summary>
|
|
public Dictionary<string, JsonNode> GetPostOverrides()
|
|
{
|
|
try
|
|
{
|
|
var row = db.ContentSections.Find(PostsKey);
|
|
if (row is null) return [];
|
|
var parsed = JsonNode.Parse(row.DataJson);
|
|
if (parsed is not JsonObject obj) return [];
|
|
return obj.ToDictionary(kv => kv.Key, kv => kv.Value!);
|
|
}
|
|
catch { return []; }
|
|
}
|
|
|
|
/// <summary>Saves a single post override.</summary>
|
|
public void SavePost(string slug, JsonNode content)
|
|
{
|
|
var row = db.ContentSections.Find(PostsKey);
|
|
JsonObject obj;
|
|
if (row is null)
|
|
{
|
|
obj = [];
|
|
row = new Models.ContentSection { Key = PostsKey };
|
|
db.ContentSections.Add(row);
|
|
}
|
|
else
|
|
{
|
|
obj = JsonNode.Parse(row.DataJson) as JsonObject ?? [];
|
|
}
|
|
obj[slug] = content.DeepClone();
|
|
row.DataJson = obj.ToJsonString();
|
|
row.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
db.SaveChanges();
|
|
}
|
|
}
|