From 0b538e1b1e6e1c387666147ee073ff850b8fc883 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Tue, 2 Jun 2026 22:24:56 +0330 Subject: [PATCH] feat(content+admin): home-events CRUD + comments moderation - content-svc: home-events gains Create/Update/Delete + includeInactive list (POST/PUT/DELETE /v1/home-events, admin-gated; dates coerced to UTC) - admin /admin/home-events: full CRUD for homepage hero event banners - admin /admin/comments: list + approve/unapprove + delete (moderation) - AdminResource: optional listQuery to fetch inactive rows for admin views Fills the remaining legacy-admin gaps (home events, comments). Co-Authored-By: Claude Opus 4.8 --- messages/en.json | 4 +- messages/fa.json | 4 +- .../Application/Services/CmsService.cs | 57 +++++++++++++++--- .../Controllers/CmsController.cs | 22 ++++++- .../Models/Requests/Requests.cs | 19 ++++++ src/app/[locale]/admin/comments/page.tsx | 8 +++ src/app/[locale]/admin/home-events/page.tsx | 8 +++ src/app/[locale]/admin/layout.tsx | 2 + src/components/admin/AdminResource.tsx | 5 +- src/components/admin/admin-resources.tsx | 59 +++++++++++++++++++ 10 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 src/app/[locale]/admin/comments/page.tsx create mode 100644 src/app/[locale]/admin/home-events/page.tsx diff --git a/messages/en.json b/messages/en.json index 03571a1..ed4a774 100644 --- a/messages/en.json +++ b/messages/en.json @@ -329,7 +329,9 @@ "crm": "CRM", "ranking": "Ranking", "stats": "Dashboard", - "music": "Music" + "music": "Music", + "homeEvents": "Home Events", + "comments": "Comments" }, "appAdminNodesPage": { "title": "Render Nodes", diff --git a/messages/fa.json b/messages/fa.json index 1999fe1..68f0b58 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -329,7 +329,9 @@ "crm": "مدیریت مشتریان", "ranking": "رتبه‌بندی", "stats": "داشبورد", - "music": "موسیقی" + "music": "موسیقی", + "homeEvents": "رویدادهای صفحه اصلی", + "comments": "نظرات" }, "appAdminNodesPage": { "title": "نودهای رندر", diff --git a/services/content/FlatRender.ContentSvc/Application/Services/CmsService.cs b/services/content/FlatRender.ContentSvc/Application/Services/CmsService.cs index c61c335..49d93ce 100644 --- a/services/content/FlatRender.ContentSvc/Application/Services/CmsService.cs +++ b/services/content/FlatRender.ContentSvc/Application/Services/CmsService.cs @@ -172,16 +172,55 @@ public class CmsService(ContentDbContext db) // ── Home Page Events ────────────────────────────────────────────────────── - public async Task> GetHomePageEventsAsync(Guid? tenantId) + public async Task> GetHomePageEventsAsync(Guid? tenantId, bool includeInactive = false) { - return await db.HomePageEvents - .Where(x => x.IsActive && (x.TenantId == null || x.TenantId == tenantId)) - .OrderBy(x => x.Sort) - .Select(e => new HomePageEventResponse( - e.Id, e.Title, e.Subtitle, e.Description, e.Badge, e.BadgeClass, - e.ButtonText, e.ButtonUrl, e.ButtonClass, e.Color, e.BackgroundColor, - e.TextColor, e.Image, e.IsActive, e.Sort, e.StartsAt, e.EndsAt - )).ToListAsync(); + var q = db.HomePageEvents.Where(x => x.TenantId == null || x.TenantId == tenantId); + if (!includeInactive) q = q.Where(x => x.IsActive); + return await q.OrderBy(x => x.Sort).Select(e => MapHomeEvent(e)).ToListAsync(); + } + + private static HomePageEventResponse MapHomeEvent(HomePageEvent e) => new( + e.Id, e.Title, e.Subtitle, e.Description, e.Badge, e.BadgeClass, + e.ButtonText, e.ButtonUrl, e.ButtonClass, e.Color, e.BackgroundColor, + e.TextColor, e.Image, e.IsActive, e.Sort, e.StartsAt, e.EndsAt); + + private static DateTime? Utc(DateTime? d) => d.HasValue ? DateTime.SpecifyKind(d.Value, DateTimeKind.Utc) : null; + + public async Task CreateHomeEventAsync(UpsertHomeEventRequest req) + { + var e = new HomePageEvent(); + ApplyHomeEvent(e, req); + db.HomePageEvents.Add(e); + await db.SaveChangesAsync(); + return MapHomeEvent(e); + } + + public async Task UpdateHomeEventAsync(Guid id, UpsertHomeEventRequest req) + { + var e = await db.HomePageEvents.FindAsync(id) + ?? throw new KeyNotFoundException($"Home event {id} not found"); + ApplyHomeEvent(e, req); + e.UpdatedAt = DateTime.UtcNow; + await db.SaveChangesAsync(); + return MapHomeEvent(e); + } + + public async Task DeleteHomeEventAsync(Guid id) + { + var e = await db.HomePageEvents.FindAsync(id) + ?? throw new KeyNotFoundException($"Home event {id} not found"); + db.HomePageEvents.Remove(e); + await db.SaveChangesAsync(); + } + + private static void ApplyHomeEvent(HomePageEvent e, UpsertHomeEventRequest r) + { + e.Title = r.Title; e.Subtitle = r.Subtitle; e.Description = r.Description; + e.Badge = r.Badge; e.BadgeClass = r.BadgeClass; + e.ButtonText = r.ButtonText; e.ButtonUrl = r.ButtonUrl; e.ButtonClass = r.ButtonClass; + e.Color = r.Color; e.BackgroundColor = r.BackgroundColor; e.TextColor = r.TextColor; + e.Image = r.Image; e.IsActive = r.IsActive; e.Sort = r.Sort; + e.StartsAt = Utc(r.StartsAt); e.EndsAt = Utc(r.EndsAt); } // ── Website Settings ────────────────────────────────────────────────────── diff --git a/services/content/FlatRender.ContentSvc/Controllers/CmsController.cs b/services/content/FlatRender.ContentSvc/Controllers/CmsController.cs index 4210e62..ac969a1 100644 --- a/services/content/FlatRender.ContentSvc/Controllers/CmsController.cs +++ b/services/content/FlatRender.ContentSvc/Controllers/CmsController.cs @@ -103,8 +103,26 @@ public class SlidesController(CmsService svc) : ControllerBase public class HomePageEventsController(CmsService svc) : ControllerBase { [HttpGet] - public async Task Get([FromQuery] Guid? tenantId = null) => - Ok(await svc.GetHomePageEventsAsync(tenantId)); + public async Task Get([FromQuery] Guid? tenantId = null, [FromQuery] bool includeInactive = false) => + Ok(await svc.GetHomePageEventsAsync(tenantId, includeInactive)); + + [Authorize(Roles = "Admin")] + [HttpPost] + public async Task Create([FromBody] UpsertHomeEventRequest req) => + Ok(await svc.CreateHomeEventAsync(req)); + + [Authorize(Roles = "Admin")] + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] UpsertHomeEventRequest req) => + Ok(await svc.UpdateHomeEventAsync(id, req)); + + [Authorize(Roles = "Admin")] + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + await svc.DeleteHomeEventAsync(id); + return NoContent(); + } } [ApiController] diff --git a/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs b/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs index f4df328..855172d 100644 --- a/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs +++ b/services/content/FlatRender.ContentSvc/Models/Requests/Requests.cs @@ -95,6 +95,25 @@ public record CreateMusicTrackRequest( int Sort ); +public record UpsertHomeEventRequest( + string? Title, + string? Subtitle, + string? Description, + string? Badge, + string? BadgeClass, + string? ButtonText, + string? ButtonUrl, + string? ButtonClass, + string? Color, + string? BackgroundColor, + string? TextColor, + string? Image, + bool IsActive = true, + int Sort = 0, + DateTime? StartsAt = null, + DateTime? EndsAt = null +); + public record UpdateMusicTrackRequest( string Name, string? Caption, diff --git a/src/app/[locale]/admin/comments/page.tsx b/src/app/[locale]/admin/comments/page.tsx new file mode 100644 index 0000000..c7bdc95 --- /dev/null +++ b/src/app/[locale]/admin/comments/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { AdminResource } from "@/components/admin/AdminResource"; +import { commentsConfig } from "@/components/admin/admin-resources"; + +export default function Page() { + return ; +} diff --git a/src/app/[locale]/admin/home-events/page.tsx b/src/app/[locale]/admin/home-events/page.tsx new file mode 100644 index 0000000..2f0dc7f --- /dev/null +++ b/src/app/[locale]/admin/home-events/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { AdminResource } from "@/components/admin/AdminResource"; +import { homeEventsConfig } from "@/components/admin/admin-resources"; + +export default function Page() { + return ; +} diff --git a/src/app/[locale]/admin/layout.tsx b/src/app/[locale]/admin/layout.tsx index 98475e3..e5ce5fc 100644 --- a/src/app/[locale]/admin/layout.tsx +++ b/src/app/[locale]/admin/layout.tsx @@ -25,6 +25,8 @@ export default async function AdminLayout({ { href: "/admin/music", label: t("music") }, { href: "/admin/blogs", label: t("blogs") }, { href: "/admin/slides", label: t("slides") }, + { href: "/admin/home-events", label: t("homeEvents") }, + { href: "/admin/comments", label: t("comments") }, { href: "/admin/files", label: t("media") }, { href: "/admin/ai", label: t("aiContent") }, { href: "/admin/messaging", label: t("messaging") }, diff --git a/src/components/admin/AdminResource.tsx b/src/components/admin/AdminResource.tsx index 695b2c9..c97b9ff 100644 --- a/src/components/admin/AdminResource.tsx +++ b/src/components/admin/AdminResource.tsx @@ -26,6 +26,7 @@ export interface ResourceConfig { basePath: string; // e.g. "categories" idKey?: string; // default "id" listKey?: string; // wrap key, e.g. "items"; omit if response is a bare array + listQuery?: string; // extra query string appended to the list fetch, e.g. "includeInactive=true" columns: ColumnDef[]; fields?: FieldDef[]; canCreate?: boolean; @@ -55,7 +56,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) { setLoading(true); setError(null); try { - const res = await fetch(url(), { cache: "no-store" }); + const res = await fetch(url(config.listQuery ? `?${config.listQuery}` : ""), { cache: "no-store" }); const data = await res.json(); if (!res.ok) throw new Error(data?.error ?? "Failed to load"); const list = config.listKey ? data?.[config.listKey] : data; @@ -66,7 +67,7 @@ export function AdminResource({ config }: { config: ResourceConfig }) { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config.basePath, config.listKey]); + }, [config.basePath, config.listKey, config.listQuery]); useEffect(() => { reload(); diff --git a/src/components/admin/admin-resources.tsx b/src/components/admin/admin-resources.tsx index a1da783..f069d90 100644 --- a/src/components/admin/admin-resources.tsx +++ b/src/components/admin/admin-resources.tsx @@ -176,6 +176,65 @@ export const slidesConfig: ResourceConfig = { ], }; +export const homeEventsConfig: ResourceConfig = { + title: "Home Events", + description: "Promotional event banners on the homepage hero.", + basePath: "home-events", + listQuery: "includeInactive=true", + canCreate: true, + canEdit: true, + canDelete: true, + columns: [ + { key: "title", label: "Title" }, + { key: "badge", label: "Badge" }, + { key: "sort", label: "Sort" }, + { key: "is_active", label: "Active", render: (r) => badge(!!r.is_active, "active", "hidden") }, + ], + fields: [ + { key: "title", label: "Title", required: true }, + { key: "subtitle", label: "Subtitle" }, + { key: "description", label: "Description", type: "textarea" }, + { key: "badge", label: "Badge text" }, + { key: "image", label: "Image", type: "image" }, + { key: "button_text", label: "Button text" }, + { key: "button_url", label: "Button URL" }, + { key: "color", label: "Text color (hex)" }, + { key: "background_color", label: "Background color (hex)" }, + { key: "sort", label: "Sort", type: "number" }, + { key: "is_active", label: "Active", type: "checkbox" }, + ], +}; + +export const commentsConfig: ResourceConfig = { + title: "Comments", + description: "Moderate user comments on blogs and templates.", + basePath: "comments", + listKey: "data", + canCreate: false, + canEdit: false, + canDelete: true, + columns: [ + { key: "author_name", label: "Author" }, + { key: "content", label: "Comment" }, + { key: "is_approved", label: "Approved", render: (r) => badge(!!r.is_approved, "approved", "pending") }, + ], + rowActions: (row, reload) => { + const id = String(row.id); + const approve = async (val: boolean) => { + await fetch(`/api/admin/resource/comments/${id}/approve?approve=${val}`, { method: "PATCH" }); + reload?.(); + }; + return ( + + ); + }, +}; + export const usersConfig: ResourceConfig = { title: "Users", description: "Accounts in this tenant. Ban or unban below.",