diff --git a/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs b/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs index f063bf0..8a5dd0d 100644 --- a/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs +++ b/services/content/FlatRender.ContentSvc/Application/Services/SceneColorService.cs @@ -174,6 +174,63 @@ public class SceneColorService(ContentDbContext db) p.Items.OrderBy(i => i.Sort).Select(i => new ColorPresetItemResponse(i.Id, i.ElementKey, i.Value, i.Sort)).ToList())) .FirstAsync(); + // ── Scene content elements (editable inputs) ──────────────────────────── + + public async Task> GetContentElementsAsync(Guid sceneId) => + await db.SceneContentElements.Where(e => e.SceneId == sceneId) + .OrderBy(e => e.PositionInContainer) + .Select(e => ToElementResponse(e)).ToListAsync(); + + public async Task CreateContentElementAsync(SaveContentElementRequest req) + { + var e = new SceneContentElement { SceneId = req.SceneId }; + ApplyElement(e, req); + db.SceneContentElements.Add(e); + await db.SaveChangesAsync(); + return ToElementResponse(e); + } + + public async Task UpdateContentElementAsync(Guid id, SaveContentElementRequest req) + { + var e = await db.SceneContentElements.FindAsync(id) + ?? throw new KeyNotFoundException($"Element {id} not found"); + ApplyElement(e, req); + await db.SaveChangesAsync(); + return ToElementResponse(e); + } + + public async Task DeleteContentElementAsync(Guid id) + { + var e = await db.SceneContentElements.FindAsync(id) + ?? throw new KeyNotFoundException($"Element {id} not found"); + db.SceneContentElements.Remove(e); + await db.SaveChangesAsync(); + } + + private static void ApplyElement(SceneContentElement e, SaveContentElementRequest req) + { + e.Key = req.Key; + e.Title = req.Title; + e.Hint = req.Hint; + e.Type = Enum.TryParse(req.Type, true, out var t) ? t : ContentElementType.Text; + e.DefaultValue = req.DefaultValue; + e.PositionInContainer = req.PositionInContainer; + e.IsTextBox = req.IsTextBox; + e.MaxSize = req.MaxSize; + e.FontSize = req.FontSize; + e.IsFontChangeable = req.IsFontChangeable; + e.IsFontSizeChangeable = req.IsFontSizeChangeable; + e.VideoSupport = req.VideoSupport; + e.Width = req.Width; + e.Height = req.Height; + e.Thumbnail = req.Thumbnail; + } + + private static ContentElementResponse ToElementResponse(SceneContentElement e) => new( + e.Id, e.SceneId, e.Key, e.Title, e.Hint, e.Type.ToString(), e.DefaultValue, + e.PositionInContainer, e.IsTextBox, e.MaxSize, e.FontSize, e.IsFontChangeable, + e.IsFontSizeChangeable, e.VideoSupport, e.Width, e.Height, e.Thumbnail); + // ── helpers ───────────────────────────────────────────────────────────── private static SceneResponse ToSceneResponse(Scene s) => new( diff --git a/services/content/FlatRender.ContentSvc/Controllers/SceneColorController.cs b/services/content/FlatRender.ContentSvc/Controllers/SceneColorController.cs index c2bd2df..5fb27f0 100644 --- a/services/content/FlatRender.ContentSvc/Controllers/SceneColorController.cs +++ b/services/content/FlatRender.ContentSvc/Controllers/SceneColorController.cs @@ -32,6 +32,33 @@ public class ScenesController(SceneColorService svc) : ControllerBase } } +[ApiController] +[Route("v1/scene-elements")] +public class SceneElementsController(SceneColorService svc) : ControllerBase +{ + [HttpGet] + public async Task List([FromQuery(Name = "scene_id")] Guid sceneId) => + Ok(await svc.GetContentElementsAsync(sceneId)); + + [Authorize(Roles = "Admin")] + [HttpPost] + public async Task Create([FromBody] SaveContentElementRequest req) => + Ok(await svc.CreateContentElementAsync(req)); + + [Authorize(Roles = "Admin")] + [HttpPut("{id:guid}")] + public async Task Update(Guid id, [FromBody] SaveContentElementRequest req) => + Ok(await svc.UpdateContentElementAsync(id, req)); + + [Authorize(Roles = "Admin")] + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + await svc.DeleteContentElementAsync(id); + return NoContent(); + } +} + [ApiController] [Route("v1/shared-colors")] public class SharedColorsController(SceneColorService svc) : ControllerBase diff --git a/services/content/FlatRender.ContentSvc/Models/SceneColor.cs b/services/content/FlatRender.ContentSvc/Models/SceneColor.cs index 3599e65..8d052af 100644 --- a/services/content/FlatRender.ContentSvc/Models/SceneColor.cs +++ b/services/content/FlatRender.ContentSvc/Models/SceneColor.cs @@ -48,3 +48,21 @@ public record ColorPresetItemInput(string ElementKey, string Value, int Sort); public record SaveColorPresetRequest( Guid ProjectId, string? Name, int Sort, List Items ); + +// ── Scene content elements (the editable inputs inside a scene) ─────────────── + +public record ContentElementResponse( + Guid Id, Guid SceneId, string Key, string Title, string? Hint, + string Type, string? DefaultValue, int PositionInContainer, + bool IsTextBox, int? MaxSize, int? FontSize, bool IsFontChangeable, + bool IsFontSizeChangeable, bool VideoSupport, int? Width, int? Height, + string? Thumbnail +); + +public record SaveContentElementRequest( + Guid SceneId, string Key, string Title, string? Hint, + string Type, string? DefaultValue, int PositionInContainer, + bool IsTextBox, int? MaxSize, int? FontSize, + bool IsFontChangeable, bool IsFontSizeChangeable, + bool VideoSupport, int? Width, int? Height, string? Thumbnail +); diff --git a/services/gateway/cmd/server/main.go b/services/gateway/cmd/server/main.go index 7a74272..2ba3114 100644 --- a/services/gateway/cmd/server/main.go +++ b/services/gateway/cmd/server/main.go @@ -118,6 +118,7 @@ func main() { v1.Any("/favorites/*path", apiRL, auth, content.Handler()) v1.Any("/ai/*path", apiRL, auth, content.Handler()) v1.Any("/scenes/*path", apiRL, optionalAuth, content.Handler()) + v1.Any("/scene-elements/*path", apiRL, optionalAuth, content.Handler()) v1.Any("/shared-colors/*path", apiRL, optionalAuth, content.Handler()) v1.Any("/color-presets/*path", apiRL, optionalAuth, content.Handler()) diff --git a/src/components/admin/SceneColorEditor.tsx b/src/components/admin/SceneColorEditor.tsx index 58b056a..821b0a1 100644 --- a/src/components/admin/SceneColorEditor.tsx +++ b/src/components/admin/SceneColorEditor.tsx @@ -204,6 +204,7 @@ function ScenesTab({ const [draft, setDraft] = useState({ ...empty }); const [editId, setEditId] = useState(null); const [busy, setBusy] = useState(false); + const [inputsFor, setInputsFor] = useState(null); const submit = async () => { setBusy(true); @@ -240,45 +241,57 @@ function ScenesTab({ .slice() .sort((a, b) => a.sort - b.sort) .map((s) => ( -
- - {s.scene_type} - - - {s.title}{" "} - - ({s.key}) +
+
+ + {s.scene_type} - - {s.default_duration_sec ?? "—"}s - #{s.sort} - {!s.is_active && ( - - غیرفعال + + {s.title}{" "} + + ({s.key}) + + {s.default_duration_sec ?? "—"}s + #{s.sort} + {!s.is_active && ( + + غیرفعال + + )} + + + +
+ {inputsFor === s.id && ( +
+ +
)} - -
))}
@@ -705,3 +718,187 @@ function PresetsTab({ ); } + +// ── Scene inputs (content elements) ─────────────────────────────────────────── + +interface ContentElement { + id: string; + scene_id: string; + key: string; + title: string; + hint?: string | null; + type: string; + default_value?: string | null; + position_in_container: number; +} + +const ELEMENT_TYPES = [ + "Text", "TextArea", "Media", "Audio", "Voiceover", "CheckBox", + "DropDown", "Fill", "Color", "Number", "Date", "Toggle", "Slider", "Counter", "Hidden", +]; + +function SceneInputsEditor({ + sceneId, + setError, +}: { + sceneId: string; + setError: (s: string | null) => void; +}) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const empty = { key: "", title: "", type: "Text", default_value: "", hint: "", position_in_container: 0 }; + const [draft, setDraft] = useState({ ...empty }); + const [editId, setEditId] = useState(null); + const [busy, setBusy] = useState(false); + + const reload = useCallback(async () => { + setLoading(true); + try { + const r = await fetch(`/api/admin/resource/scene-elements?scene_id=${sceneId}`, { + cache: "no-store", + }).then((x) => x.json()); + setItems(Array.isArray(r) ? r : (r?.items ?? [])); + } catch { + setError("بارگذاری ورودی‌ها ناموفق بود"); + } finally { + setLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sceneId]); + + useEffect(() => { + reload(); + }, [reload]); + + const submit = async () => { + setBusy(true); + setError(null); + const body = { scene_id: sceneId, is_text_box: draft.type === "TextArea", ...draft }; + const url = editId + ? `/api/admin/resource/scene-elements/${editId}` + : `/api/admin/resource/scene-elements`; + const res = await fetch(url, { + method: editId ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + setBusy(false); + if (res.ok) { + setDraft({ ...empty }); + setEditId(null); + reload(); + } else { + setError("ذخیرهٔ ورودی ناموفق بود"); + } + }; + + const remove = async (el: ContentElement) => { + if (!confirm(`ورودی «${el.title}» حذف شود؟`)) return; + const res = await fetch(`/api/admin/resource/scene-elements/${el.id}`, { method: "DELETE" }); + if (res.ok) reload(); + }; + + return ( +
+

+ ورودی‌های قابل ویرایش این صحنه (متن، تصویر، رنگ …) +

+ {loading ? ( +

در حال بارگذاری…

+ ) : ( +
+ {items.length === 0 && ( +

هنوز ورودی‌ای تعریف نشده است.

+ )} + {items.map((el) => ( +
+ + {el.type} + + + {el.title}{" "} + ({el.key}) + + {el.default_value && ( + + {el.default_value} + + )} + #{el.position_in_container} + + +
+ ))} +
+ )} + + {/* Add / edit input */} +
+
+
+ + setDraft({ ...draft, key: e.target.value })} /> +
+
+ + setDraft({ ...draft, title: e.target.value })} /> +
+
+ + +
+
+ + setDraft({ ...draft, default_value: e.target.value })} /> +
+
+ + setDraft({ ...draft, position_in_container: Number(e.target.value) })} /> +
+
+ + setDraft({ ...draft, hint: e.target.value })} /> +
+
+
+ + {editId && ( + + )} +
+
+
+ ); +}