Skip to content

Commit cdcfbcb

Browse files
committed
Add live preview mode to page editor
1 parent ac0e0bc commit cdcfbcb

5 files changed

Lines changed: 151 additions & 1 deletion

File tree

website/MyWebApp/Controllers/AdminContentController.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ public class AdminContentController : Controller
1717
private readonly ApplicationDbContext _db;
1818
private readonly LayoutService _layout;
1919
private readonly ContentProcessingService _content;
20+
private readonly TokenRenderService _tokens;
2021

21-
public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content)
22+
public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens)
2223
{
2324
_db = db;
2425
_layout = layout;
2526
_content = content;
27+
_tokens = tokens;
2628
}
2729

2830
private async Task LoadTemplatesAsync()
@@ -99,6 +101,21 @@ public async Task<IActionResult> Create(Page model)
99101
return RedirectToAction(nameof(Index));
100102
}
101103

104+
[HttpPost]
105+
public async Task<IActionResult> Preview([FromBody] PreviewRequest model)
106+
{
107+
var zones = new Dictionary<string, string>();
108+
foreach (var kv in model.Zones ?? new Dictionary<string, string>())
109+
{
110+
zones[kv.Key] = await _tokens.RenderAsync(_db, kv.Value ?? string.Empty);
111+
}
112+
ViewBag.HeaderHtml = await _layout.GetHeaderAsync(_db);
113+
ViewBag.FooterHtml = await _layout.GetFooterAsync(_db);
114+
ViewBag.PageLayout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout;
115+
ViewBag.ZoneHtml = zones;
116+
return View("~/Views/Pages/Show.cshtml", new Page { Title = model.Title });
117+
}
118+
102119
public async Task<IActionResult> Edit(int id)
103120
{
104121
var page = await _db.Pages.FindAsync(id);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Collections.Generic;
2+
3+
namespace MyWebApp.Models;
4+
5+
public class PreviewRequest
6+
{
7+
public string Layout { get; set; } = "single-column";
8+
public string Title { get; set; } = string.Empty;
9+
public Dictionary<string, string> Zones { get; set; } = new();
10+
}
11+

website/MyWebApp/Views/AdminContent/PageEditor.cshtml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
ViewData["Title"] = isNew ? "Create Page" : "Edit Page";
99
}
1010
<h2>@ViewData["Title"]</h2>
11+
<div class="mode-toggle">
12+
<button type="button" id="mode-edit" class="mode-btn active">Edit</button>
13+
<button type="button" id="mode-preview" class="mode-btn">Preview<span id="unsaved-indicator" class="unsaved-indicator">*</span></button>
14+
<div class="device-buttons">
15+
<button type="button" class="device-btn active" data-width="100%">Desktop</button>
16+
<button type="button" class="device-btn" data-width="768px">Tablet</button>
17+
<button type="button" class="device-btn" data-width="375px">Mobile</button>
18+
</div>
19+
</div>
1120
<div class="page-editor">
1221
<aside id="block-library" class="block-library">
1322
<input type="text" id="block-search" class="search" placeholder="Search blocks..." />
@@ -69,6 +78,11 @@
6978
</div>
7079
<button type="submit">Save</button>
7180
</form>
81+
<div id="preview-wrapper" class="preview-wrapper">
82+
<div id="preview-container" class="preview-container">
83+
<iframe id="preview-frame"></iframe>
84+
</div>
85+
</div>
7286
</div>
7387
@section Scripts {
7488
<script>

website/MyWebApp/wwwroot/css/admin.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,3 +1355,29 @@ form.mb-3 {
13551355
text-overflow: ellipsis;
13561356
margin-bottom: 0.25rem;
13571357
}
1358+
\n
1359+
/* Live preview styles */
1360+
.mode-toggle {
1361+
margin-bottom: 0.5rem;
1362+
display: flex;
1363+
gap: 0.5rem;
1364+
align-items: center;
1365+
}
1366+
.mode-toggle .device-buttons {
1367+
margin-left: auto;
1368+
display: flex;
1369+
gap: 0.25rem;
1370+
}
1371+
.mode-btn.active,
1372+
.device-btn.active {
1373+
background: #0ea5e9;
1374+
color: #fff;
1375+
}
1376+
.unsaved-indicator { color: #dc2626; margin-left: 0.25rem; display: none; }
1377+
.preview-wrapper { display: none; margin-top: 1rem; }
1378+
.page-editor.preview .editor-main,
1379+
.page-editor.preview .block-library { display: none; }
1380+
.page-editor.preview .preview-wrapper { display: block; }
1381+
.preview-container { border: 1px solid #e2e8f0; margin: 0 auto; }
1382+
.preview-container iframe { width: 100%; height: 600px; border: none; }
1383+

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ window.addEventListener('load', () => {
77
let activeIndex = null;
88
const layoutSelect = document.getElementById('layout-select');
99
let currentLayout = layoutSelect ? layoutSelect.value : 'single-column';
10+
const pageEditor = document.querySelector('.page-editor');
11+
const previewWrapper = document.getElementById('preview-wrapper');
12+
const previewFrame = document.getElementById('preview-frame');
13+
const unsavedIndicator = document.getElementById('unsaved-indicator');
14+
const modeEdit = document.getElementById('mode-edit');
15+
const modePreview = document.getElementById('mode-preview');
16+
const deviceBtns = document.querySelectorAll('.device-btn');
17+
const previewContainer = document.getElementById('preview-container');
18+
let dirty = false;
19+
let previewTimer = null;
1020

1121
function buildGroups() {
1222
container.innerHTML = '';
@@ -32,6 +42,53 @@ window.addEventListener('load', () => {
3242
});
3343
}
3444

45+
function collectData() {
46+
const zones = {};
47+
document.querySelectorAll('.zone-group').forEach(g => {
48+
const zone = g.dataset.zone;
49+
const html = Array.from(g.querySelectorAll('.section-editor')).map(sec => {
50+
const idx = sec.dataset.index;
51+
const typeSel = document.getElementById(`type-select-${idx}`);
52+
if (typeSel && typeSel.value === 'Html') {
53+
return editors[idx].root.innerHTML;
54+
}
55+
const inp = document.getElementById(`Html-${idx}`);
56+
return inp ? inp.value : '';
57+
}).join('\n');
58+
zones[zone] = html;
59+
});
60+
return {
61+
layout: layoutSelect ? layoutSelect.value : 'single-column',
62+
title: document.getElementById('title-input')?.value || '',
63+
zones
64+
};
65+
}
66+
67+
function renderPreview() {
68+
const data = collectData();
69+
fetch('/AdminContent/Preview', {
70+
method: 'POST',
71+
headers: {
72+
'Content-Type': 'application/json',
73+
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
74+
},
75+
body: JSON.stringify(data)
76+
})
77+
.then(r => r.text())
78+
.then(html => {
79+
if (previewFrame) previewFrame.srcdoc = html;
80+
dirty = false;
81+
if (unsavedIndicator) unsavedIndicator.style.display = 'none';
82+
});
83+
}
84+
85+
function schedulePreview() {
86+
dirty = true;
87+
if (unsavedIndicator) unsavedIndicator.style.display = 'inline';
88+
clearTimeout(previewTimer);
89+
previewTimer = setTimeout(renderPreview, 300);
90+
}
91+
3592
function populateZones(select) {
3693
if (!select) return;
3794
const current = select.dataset.selected || select.value;
@@ -322,6 +379,7 @@ window.addEventListener('load', () => {
322379
quill.root.addEventListener('click', () => { activeIndex = index; });
323380
quill.root.addEventListener('focus', () => { activeIndex = index; });
324381
editors[index] = quill;
382+
quill.on('text-change', schedulePreview);
325383

326384
function update() {
327385
const type = typeSelect.value;
@@ -338,4 +396,28 @@ window.addEventListener('load', () => {
338396
update();
339397
typeSelect.addEventListener('change', update);
340398
}
399+
400+
modePreview?.addEventListener('click', () => {
401+
pageEditor?.classList.add('preview');
402+
renderPreview();
403+
});
404+
405+
modeEdit?.addEventListener('click', () => {
406+
pageEditor?.classList.remove('preview');
407+
});
408+
409+
deviceBtns.forEach(btn => {
410+
btn.addEventListener('click', () => {
411+
deviceBtns.forEach(b => b.classList.remove('active'));
412+
btn.classList.add('active');
413+
if (previewContainer) previewContainer.style.width = btn.dataset.width;
414+
});
415+
});
416+
417+
document.querySelector('.editor-main')?.addEventListener('input', schedulePreview);
418+
document.querySelector('.editor-main')?.addEventListener('change', schedulePreview);
419+
form?.addEventListener('submit', () => {
420+
dirty = false;
421+
if (unsavedIndicator) unsavedIndicator.style.display = 'none';
422+
});
341423
});

0 commit comments

Comments
 (0)