Skip to content

Commit ac0e0bc

Browse files
authored
Merge pull request #115 from Denis-RZ/codex/implement-visual-block-library-panel
Implement visual block library panel
2 parents 4809cb0 + e5a3bd9 commit ac0e0bc

6 files changed

Lines changed: 169 additions & 8 deletions

File tree

website/MyWebApp/Controllers/AdminBlockTemplateController.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,30 @@ public async Task<IActionResult> Import(IFormFile? file)
124124
return RedirectToAction(nameof(Index));
125125
}
126126

127+
[HttpGet]
128+
public async Task<IActionResult> GetBlocks()
129+
{
130+
var items = await _db.BlockTemplates.AsNoTracking()
131+
.OrderBy(t => t.Name)
132+
.Select(t => new { t.Id, t.Name, Preview = t.Html.Length > 200 ? t.Html.Substring(0, 200) + "..." : t.Html })
133+
.ToListAsync();
134+
return Json(items);
135+
}
136+
137+
[HttpPost]
138+
[ValidateAntiForgeryToken]
139+
public async Task<IActionResult> CreateFromSection(string name, string html)
140+
{
141+
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(html))
142+
return BadRequest();
143+
html = _sanitizer.Sanitize(html);
144+
var t = new BlockTemplate { Name = name, Html = html };
145+
_db.BlockTemplates.Add(t);
146+
_db.BlockTemplateVersions.Add(new BlockTemplateVersion { Template = t, Html = html });
147+
await _db.SaveChangesAsync();
148+
return Json(new { t.Id });
149+
}
150+
127151
public async Task<IActionResult> AddToPage(int id)
128152
{
129153
var item = await _db.BlockTemplates.FindAsync(id);

website/MyWebApp/Views/AdminContent/PageEditor.cshtml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
ViewData["Title"] = isNew ? "Create Page" : "Edit Page";
99
}
1010
<h2>@ViewData["Title"]</h2>
11-
<form asp-action="@(isNew ? "Create" : "Edit")" method="post">
11+
<div class="page-editor">
12+
<aside id="block-library" class="block-library">
13+
<input type="text" id="block-search" class="search" placeholder="Search blocks..." />
14+
<div class="block-list"></div>
15+
</aside>
16+
<form asp-action="@(isNew ? "Create" : "Edit")" method="post" class="editor-main">
1217
<input type="hidden" asp-for="Id" />
1318
<div>
1419
<div><label>Slug</label><input asp-for="Slug" id="slug-input" /></div>
@@ -63,14 +68,16 @@
6368
@await Html.PartialAsync("~/Views/Shared/_SectionEditor.cshtml", new PageSection(), new ViewDataDictionary(ViewData) { ["Index"] = "__index__" })
6469
</div>
6570
<button type="submit">Save</button>
66-
</form>
71+
</form>
72+
</div>
6773
@section Scripts {
6874
<script>
6975
const layoutZones = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
7076
ViewBag.LayoutZones as IReadOnlyDictionary<string, string[]> ??
7177
new Dictionary<string, string[]>()));
7278
</script>
7379
<script src="~/js/page-editor.js" asp-append-version="true"></script>
80+
<script src="~/js/block-library.js" asp-append-version="true"></script>
7481
<script>
7582
const titleInput = document.getElementById('title-input');
7683
const slugInput = document.getElementById('slug-input');

website/MyWebApp/Views/Shared/_SectionEditor.cshtml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
<div id="code-editor" class="type-editor">
1515
<textarea asp-for="Html"></textarea>
1616
</div>
17-
<div id="file-editor" class="type-editor">
18-
<input type="file" name="file" />
19-
</div>
20-
<script>
17+
<div id="file-editor" class="type-editor">
18+
<input type="file" name="file" />
19+
</div>
20+
<button type="button" class="add-library">Add to Library</button>
21+
<script>
2122
const typeSelect = document.getElementById('type-select');
2223
const htmlDiv = document.getElementById('html-editor');
2324
const markdownDiv = document.getElementById('markdown-editor');

website/MyWebApp/wwwroot/css/admin.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,3 +1315,43 @@ form.mb-3 {
13151315
}
13161316
.zone-group[data-zone="main"] { border-color: #2563eb; }
13171317
.zone-group[data-zone="sidebar"] { border-color: #16a34a; }
1318+
1319+
/* Block library panel */
1320+
.page-editor {
1321+
display: flex;
1322+
gap: 1rem;
1323+
}
1324+
1325+
.block-library {
1326+
width: 220px;
1327+
flex-shrink: 0;
1328+
border-right: 1px solid #e2e8f0;
1329+
padding-right: 1rem;
1330+
margin-right: 1rem;
1331+
}
1332+
1333+
.block-library .search {
1334+
width: 100%;
1335+
margin-bottom: 0.5rem;
1336+
}
1337+
1338+
.block-card {
1339+
border: 1px solid #cbd5e1;
1340+
background: #fff;
1341+
padding: 0.5rem;
1342+
margin-bottom: 0.5rem;
1343+
cursor: grab;
1344+
}
1345+
1346+
.block-card:hover {
1347+
box-shadow: 0 0 0 2px #0ea5e9 inset;
1348+
}
1349+
1350+
.block-card .block-preview {
1351+
font-size: 0.8rem;
1352+
color: #555;
1353+
overflow: hidden;
1354+
white-space: nowrap;
1355+
text-overflow: ellipsis;
1356+
margin-bottom: 0.25rem;
1357+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
window.addEventListener('load', () => {
2+
const panel = document.getElementById('block-library');
3+
if (!panel) return;
4+
const search = document.getElementById('block-search');
5+
const list = panel.querySelector('.block-list');
6+
let blocks = [];
7+
fetch('/AdminBlockTemplate/GetBlocks')
8+
.then(r => r.json())
9+
.then(data => { blocks = data; render(blocks); });
10+
11+
function render(items) {
12+
list.innerHTML = items.map(b => {
13+
const encoded = b.preview.replace(/</g, '&lt;').replace(/>/g, '&gt;');
14+
return `<div class="block-card" draggable="true" data-id="${b.id}">
15+
<div class="block-name">${b.name}</div>
16+
<div class="block-preview">${encoded}</div>
17+
<button type="button" class="quick-edit" data-id="${b.id}">Edit</button>
18+
</div>`;
19+
}).join('');
20+
}
21+
22+
search?.addEventListener('input', () => {
23+
const q = search.value.toLowerCase();
24+
const filtered = blocks.filter(b => b.name.toLowerCase().includes(q));
25+
render(filtered);
26+
});
27+
28+
list.addEventListener('click', e => {
29+
const btn = e.target.closest('.quick-edit');
30+
if (btn) {
31+
const id = btn.dataset.id;
32+
window.location = `/AdminBlockTemplate/Edit/${id}`;
33+
}
34+
});
35+
36+
list.addEventListener('dragstart', e => {
37+
const card = e.target.closest('.block-card');
38+
if (!card) return;
39+
window.draggedBlockId = card.dataset.id;
40+
e.dataTransfer.effectAllowed = 'copy';
41+
});
42+
});

website/MyWebApp/wwwroot/js/page-editor.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ window.addEventListener('load', () => {
133133
} else if (e.target.classList.contains('duplicate-section')) {
134134
const original = e.target.closest('.section-editor');
135135
duplicateSection(original);
136+
} else if (e.target.classList.contains('add-library')) {
137+
const section = e.target.closest('.section-editor');
138+
const idx = section.dataset.index;
139+
const name = prompt('Block name');
140+
if (!name) return;
141+
const html = editors[idx].root.innerHTML;
142+
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
143+
fetch('/AdminBlockTemplate/CreateFromSection', {
144+
method: 'POST',
145+
headers: { 'RequestVerificationToken': token },
146+
body: new URLSearchParams({ name, html })
147+
}).then(() => alert('Saved to library'));
136148
}
137149
});
138150

@@ -145,7 +157,7 @@ window.addEventListener('load', () => {
145157
}
146158
});
147159

148-
function addSection() {
160+
function addSection(htmlContent = '', zone = null) {
149161
const index = sectionCount++;
150162
const html = templateHtml.replace(/__index__/g, index);
151163
const temp = document.createElement('div');
@@ -155,9 +167,29 @@ window.addEventListener('load', () => {
155167
populateZones(section.querySelector('.zone-select'));
156168
placeSection(section);
157169
initSectionEditor(index);
170+
if (htmlContent) {
171+
editors[index].root.innerHTML = htmlContent;
172+
const input = document.getElementById(`Html-${index}`);
173+
if (input) input.value = htmlContent;
174+
}
175+
if (zone) {
176+
const select = section.querySelector('.zone-select');
177+
if (select) { select.value = zone; }
178+
placeSection(section);
179+
}
158180
updateIndexes();
159181
updatePreview();
160182
updateZoneCounts();
183+
return index;
184+
}
185+
186+
function addSectionFromBlock(id, zone) {
187+
fetch(`/AdminBlockTemplate/Html/${id}`)
188+
.then(r => r.text())
189+
.then(html => {
190+
addSection(html, zone);
191+
autoSave();
192+
});
161193
}
162194

163195
function duplicateSection(original) {
@@ -204,11 +236,13 @@ window.addEventListener('load', () => {
204236
}
205237

206238
let dragged = null;
239+
let draggedBlockId = null;
207240
const dropIndicator = document.createElement('div');
208241
dropIndicator.className = 'drop-indicator';
209242

210243
container.addEventListener('dragstart', e => {
211244
dragged = e.target.closest('.section-editor');
245+
draggedBlockId = null;
212246
if (dragged) {
213247
dragged.classList.add('dragging');
214248
document.querySelectorAll('.zone-group').forEach(z => z.classList.add('drag-over'));
@@ -221,7 +255,7 @@ window.addEventListener('load', () => {
221255
const zone = e.target.closest('.zone-group');
222256
const target = e.target.closest('.section-editor');
223257
if (zone) zone.classList.add('drag-over');
224-
if (dragged && target && target !== dragged) {
258+
if (!draggedBlockId && dragged && target && target !== dragged) {
225259
const rect = target.getBoundingClientRect();
226260
const next = (e.clientY - rect.top) > (rect.height / 2);
227261
target.parentNode.insertBefore(dropIndicator, next ? target.nextSibling : target);
@@ -237,6 +271,13 @@ window.addEventListener('load', () => {
237271

238272
container.addEventListener('drop', e => {
239273
e.preventDefault();
274+
const zone = e.target.closest('.zone-group');
275+
if (draggedBlockId && zone) {
276+
addSectionFromBlock(draggedBlockId, zone.dataset.zone);
277+
draggedBlockId = null;
278+
document.querySelectorAll('.zone-group.drag-over').forEach(z => z.classList.remove('drag-over'));
279+
return;
280+
}
240281
if (dropIndicator.parentNode) {
241282
dropIndicator.parentNode.insertBefore(dragged, dropIndicator);
242283
dropIndicator.remove();
@@ -261,6 +302,12 @@ window.addEventListener('load', () => {
261302
});
262303
}
263304

305+
function autoSave() {
306+
if (!form) return;
307+
const fd = new FormData(form);
308+
fetch(form.action, { method: 'POST', body: fd });
309+
}
310+
264311
function initSectionEditor(index) {
265312
const typeSelect = document.getElementById(`type-select-${index}`);
266313
const htmlDiv = document.getElementById(`html-editor-${index}`);

0 commit comments

Comments
 (0)