Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion website/MyWebApp/Controllers/AdminContentController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ public class AdminContentController : Controller
private readonly ApplicationDbContext _db;
private readonly LayoutService _layout;
private readonly ContentProcessingService _content;
private readonly TokenRenderService _tokens;

public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content)
public AdminContentController(ApplicationDbContext db, LayoutService layout, ContentProcessingService content, TokenRenderService tokens)
{
_db = db;
_layout = layout;
_content = content;
_tokens = tokens;
}

private async Task LoadTemplatesAsync()
Expand Down Expand Up @@ -99,6 +101,21 @@ public async Task<IActionResult> Create(Page model)
return RedirectToAction(nameof(Index));
}

[HttpPost]
public async Task<IActionResult> Preview([FromBody] PreviewRequest model)
{
var zones = new Dictionary<string, string>();
foreach (var kv in model.Zones ?? new Dictionary<string, string>())
{
zones[kv.Key] = await _tokens.RenderAsync(_db, kv.Value ?? string.Empty);
}
ViewBag.HeaderHtml = await _layout.GetHeaderAsync(_db);
ViewBag.FooterHtml = await _layout.GetFooterAsync(_db);
ViewBag.PageLayout = string.IsNullOrWhiteSpace(model.Layout) ? "single-column" : model.Layout;
ViewBag.ZoneHtml = zones;
return View("~/Views/Pages/Show.cshtml", new Page { Title = model.Title });
}

public async Task<IActionResult> Edit(int id)
{
var page = await _db.Pages.FindAsync(id);
Expand Down
11 changes: 11 additions & 0 deletions website/MyWebApp/Models/PreviewRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;

namespace MyWebApp.Models;

public class PreviewRequest
{
public string Layout { get; set; } = "single-column";
public string Title { get; set; } = string.Empty;
public Dictionary<string, string> Zones { get; set; } = new();
}

14 changes: 14 additions & 0 deletions website/MyWebApp/Views/AdminContent/PageEditor.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
ViewData["Title"] = isNew ? "Create Page" : "Edit Page";
}
<h2>@ViewData["Title"]</h2>
<div class="mode-toggle">
<button type="button" id="mode-edit" class="mode-btn active">Edit</button>
<button type="button" id="mode-preview" class="mode-btn">Preview<span id="unsaved-indicator" class="unsaved-indicator">*</span></button>
<div class="device-buttons">
<button type="button" class="device-btn active" data-width="100%">Desktop</button>
<button type="button" class="device-btn" data-width="768px">Tablet</button>
<button type="button" class="device-btn" data-width="375px">Mobile</button>
</div>
</div>
<div class="page-editor">
<aside id="block-library" class="block-library">
<input type="text" id="block-search" class="search" placeholder="Search blocks..." />
Expand Down Expand Up @@ -69,6 +78,11 @@
</div>
<button type="submit">Save</button>
</form>
<div id="preview-wrapper" class="preview-wrapper">
<div id="preview-container" class="preview-container">
<iframe id="preview-frame"></iframe>
</div>
</div>
</div>
@section Scripts {
<script>
Expand Down
26 changes: 26 additions & 0 deletions website/MyWebApp/wwwroot/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -1355,3 +1355,29 @@ form.mb-3 {
text-overflow: ellipsis;
margin-bottom: 0.25rem;
}
\n
/* Live preview styles */
.mode-toggle {
margin-bottom: 0.5rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.mode-toggle .device-buttons {
margin-left: auto;
display: flex;
gap: 0.25rem;
}
.mode-btn.active,
.device-btn.active {
background: #0ea5e9;
color: #fff;
}
.unsaved-indicator { color: #dc2626; margin-left: 0.25rem; display: none; }
.preview-wrapper { display: none; margin-top: 1rem; }
.page-editor.preview .editor-main,
.page-editor.preview .block-library { display: none; }
.page-editor.preview .preview-wrapper { display: block; }
.preview-container { border: 1px solid #e2e8f0; margin: 0 auto; }
.preview-container iframe { width: 100%; height: 600px; border: none; }

82 changes: 82 additions & 0 deletions website/MyWebApp/wwwroot/js/page-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ window.addEventListener('load', () => {
let activeIndex = null;
const layoutSelect = document.getElementById('layout-select');
let currentLayout = layoutSelect ? layoutSelect.value : 'single-column';
const pageEditor = document.querySelector('.page-editor');
const previewWrapper = document.getElementById('preview-wrapper');
const previewFrame = document.getElementById('preview-frame');
const unsavedIndicator = document.getElementById('unsaved-indicator');
const modeEdit = document.getElementById('mode-edit');
const modePreview = document.getElementById('mode-preview');
const deviceBtns = document.querySelectorAll('.device-btn');
const previewContainer = document.getElementById('preview-container');
let dirty = false;
let previewTimer = null;

function buildGroups() {
container.innerHTML = '';
Expand All @@ -32,6 +42,53 @@ window.addEventListener('load', () => {
});
}

function collectData() {
const zones = {};
document.querySelectorAll('.zone-group').forEach(g => {
const zone = g.dataset.zone;
const html = Array.from(g.querySelectorAll('.section-editor')).map(sec => {
const idx = sec.dataset.index;
const typeSel = document.getElementById(`type-select-${idx}`);
if (typeSel && typeSel.value === 'Html') {
return editors[idx].root.innerHTML;
}
const inp = document.getElementById(`Html-${idx}`);
return inp ? inp.value : '';
}).join('\n');
zones[zone] = html;
});
return {
layout: layoutSelect ? layoutSelect.value : 'single-column',
title: document.getElementById('title-input')?.value || '',
zones
};
}

function renderPreview() {
const data = collectData();
fetch('/AdminContent/Preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify(data)
})
.then(r => r.text())
.then(html => {
if (previewFrame) previewFrame.srcdoc = html;
dirty = false;
if (unsavedIndicator) unsavedIndicator.style.display = 'none';
});
}

function schedulePreview() {
dirty = true;
if (unsavedIndicator) unsavedIndicator.style.display = 'inline';
clearTimeout(previewTimer);
previewTimer = setTimeout(renderPreview, 300);
}

function populateZones(select) {
if (!select) return;
const current = select.dataset.selected || select.value;
Expand Down Expand Up @@ -322,6 +379,7 @@ window.addEventListener('load', () => {
quill.root.addEventListener('click', () => { activeIndex = index; });
quill.root.addEventListener('focus', () => { activeIndex = index; });
editors[index] = quill;
quill.on('text-change', schedulePreview);

function update() {
const type = typeSelect.value;
Expand All @@ -338,4 +396,28 @@ window.addEventListener('load', () => {
update();
typeSelect.addEventListener('change', update);
}

modePreview?.addEventListener('click', () => {
pageEditor?.classList.add('preview');
renderPreview();
});

modeEdit?.addEventListener('click', () => {
pageEditor?.classList.remove('preview');
});

deviceBtns.forEach(btn => {
btn.addEventListener('click', () => {
deviceBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
if (previewContainer) previewContainer.style.width = btn.dataset.width;
});
});

document.querySelector('.editor-main')?.addEventListener('input', schedulePreview);
document.querySelector('.editor-main')?.addEventListener('change', schedulePreview);
form?.addEventListener('submit', () => {
dirty = false;
if (unsavedIndicator) unsavedIndicator.style.display = 'none';
});
});
Loading