Initial dev export (exclude uploads/runtime)
This commit is contained in:
339
modules/admin/views/navigation.php
Normal file
339
modules/admin/views/navigation.php
Normal file
@@ -0,0 +1,339 @@
|
||||
<?php
|
||||
$pageTitle = 'Navigation';
|
||||
$links = $links ?? [];
|
||||
$pages = $pages ?? [];
|
||||
$error = $error ?? '';
|
||||
$saved = ($saved ?? '') === '1';
|
||||
ob_start();
|
||||
?>
|
||||
<section class="admin-card">
|
||||
<div class="badge">Navigation</div>
|
||||
<h1 style="margin-top:16px; font-size:28px;">Site Navigation</h1>
|
||||
<p style="color: var(--muted); margin-top:8px;">Build your main menu. Add items on the left, then drag to reorder.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div style="margin-top:16px; color: #f3b0b0; font-size:13px;"><?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?></div>
|
||||
<?php elseif ($saved): ?>
|
||||
<div style="margin-top:16px; color: var(--accent); font-size:13px;">Navigation saved.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<style>
|
||||
.nav-grid { display: grid; grid-template-columns: 320px 1fr; gap: 18px; }
|
||||
.nav-list { display: grid; gap: 10px; }
|
||||
.nav-item {
|
||||
display: grid;
|
||||
grid-template-columns: 34px 1.3fr 2fr 90px 90px 90px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--stroke);
|
||||
background: rgba(14,14,16,0.9);
|
||||
}
|
||||
.nav-item.dragging { opacity: 0.6; }
|
||||
.drag-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
background: rgba(255,255,255,0.04);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
.nav-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
.nav-item input[readonly] { opacity: 0.7; }
|
||||
@media (max-width: 980px) {
|
||||
.nav-grid { grid-template-columns: 1fr; }
|
||||
.nav-item { grid-template-columns: 28px 1fr; grid-auto-rows: auto; }
|
||||
.nav-item > *:nth-child(n+3) { grid-column: 2 / -1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<form method="post" action="/admin/navigation" style="margin-top:20px; display:grid; gap:18px;">
|
||||
<div class="nav-grid">
|
||||
<aside class="admin-card" style="padding:16px;">
|
||||
<div class="badge" style="opacity:0.7;">Add menu items</div>
|
||||
<div style="margin-top:12px; display:grid; gap:12px;">
|
||||
<label class="label">Page picker</label>
|
||||
<select id="navPageSelect" class="input" style="text-transform:none;">
|
||||
<option value="" data-label="" data-url="">Select page</option>
|
||||
<option value="home" data-label="Home" data-url="/">Home</option>
|
||||
<option value="artists" data-label="Artists" data-url="/artists">Artists</option>
|
||||
<option value="releases" data-label="Releases" data-url="/releases">Releases</option>
|
||||
<option value="store" data-label="Store" data-url="/store">Store</option>
|
||||
<option value="contact" data-label="Contact" data-url="/contact">Contact</option>
|
||||
<?php foreach ($pages as $page): ?>
|
||||
<option value="page-<?= htmlspecialchars((string)($page['slug'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-label="<?= htmlspecialchars((string)($page['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
||||
data-url="/<?= htmlspecialchars((string)($page['slug'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= htmlspecialchars((string)($page['title'] ?? ''), ENT_QUOTES, 'UTF-8') ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option value="custom" data-label="" data-url="">Custom link</option>
|
||||
</select>
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
|
||||
<input id="navActiveInput" type="checkbox" checked>
|
||||
Active
|
||||
</label>
|
||||
<button type="button" id="navAddButton" class="btn" style="padding:8px 14px;">Add to menu</button>
|
||||
<div style="font-size:12px; color:var(--muted);">Use Custom for external links or anchors.</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="admin-card" style="padding:16px;">
|
||||
<div class="badge" style="opacity:0.7;">Menu structure</div>
|
||||
<div style="margin-top:12px; display:grid; gap:12px;">
|
||||
<div class="nav-item" style="background: transparent; border: none; padding: 0;">
|
||||
<div></div>
|
||||
<div class="nav-meta">Label</div>
|
||||
<div class="nav-meta">URL</div>
|
||||
<div class="nav-meta">Order</div>
|
||||
<div class="nav-meta">Active</div>
|
||||
<div class="nav-meta">Delete</div>
|
||||
</div>
|
||||
<div id="navMenuEmpty" style="color: var(--muted); font-size:13px; display: <?= $links ? 'none' : 'block' ?>;">No navigation links yet.</div>
|
||||
<div id="navMenuList" class="nav-list">
|
||||
<?php foreach ($links as $link): ?>
|
||||
<div class="nav-item" draggable="true">
|
||||
<div class="drag-handle" title="Drag to reorder">||</div>
|
||||
<input class="input" name="items[<?= (int)$link['id'] ?>][label]" value="<?= htmlspecialchars((string)($link['label'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<input class="input" name="items[<?= (int)$link['id'] ?>][url]" value="<?= htmlspecialchars((string)($link['url'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<input class="input" name="items[<?= (int)$link['id'] ?>][sort_order]" data-sort-input value="<?= htmlspecialchars((string)($link['sort_order'] ?? 0), ENT_QUOTES, 'UTF-8') ?>" readonly>
|
||||
<label style="display:flex; justify-content:center;">
|
||||
<input type="checkbox" name="items[<?= (int)$link['id'] ?>][is_active]" value="1" <?= ((int)($link['is_active'] ?? 0) === 1) ? 'checked' : '' ?>>
|
||||
</label>
|
||||
<label style="display:flex; justify-content:center;">
|
||||
<input type="checkbox" name="delete_ids[]" value="<?= (int)$link['id'] ?>">
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end;">
|
||||
<button type="submit" class="btn">Save navigation</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div id="navModal" style="position:fixed; inset:0; background:rgba(0,0,0,0.55); display:none; align-items:center; justify-content:center; padding:24px; z-index:40;">
|
||||
<div class="admin-card" style="max-width:520px; width:100%; position:relative;">
|
||||
<div class="badge">Custom link</div>
|
||||
<h2 style="margin-top:12px; font-size:24px;">Add custom link</h2>
|
||||
<div style="display:grid; gap:14px; margin-top:16px;">
|
||||
<div>
|
||||
<label class="label">Label</label>
|
||||
<input id="modalLabel" class="input" placeholder="Press Kit">
|
||||
</div>
|
||||
<div>
|
||||
<label class="label">URL</label>
|
||||
<input id="modalUrl" class="input" placeholder="https://example.com/press">
|
||||
</div>
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); text-transform:uppercase; letter-spacing:0.2em;">
|
||||
<input id="modalActive" type="checkbox" checked>
|
||||
Active
|
||||
</label>
|
||||
<div style="display:flex; gap:12px; justify-content:flex-end;">
|
||||
<button type="button" id="modalCancel" class="btn" style="background:transparent; color:var(--text); border:1px solid rgba(255,255,255,0.2);">Cancel</button>
|
||||
<button type="button" id="modalAdd" class="btn">Add link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
const menuList = document.getElementById('navMenuList');
|
||||
const menuEmpty = document.getElementById('navMenuEmpty');
|
||||
const addButton = document.getElementById('navAddButton');
|
||||
const pageSelect = document.getElementById('navPageSelect');
|
||||
const activeInput = document.getElementById('navActiveInput');
|
||||
|
||||
const modal = document.getElementById('navModal');
|
||||
const modalLabel = document.getElementById('modalLabel');
|
||||
const modalUrl = document.getElementById('modalUrl');
|
||||
const modalActive = document.getElementById('modalActive');
|
||||
const modalAdd = document.getElementById('modalAdd');
|
||||
const modalCancel = document.getElementById('modalCancel');
|
||||
|
||||
let stagedIndex = 0;
|
||||
|
||||
function showModal() {
|
||||
modal.style.display = 'flex';
|
||||
modalLabel.value = '';
|
||||
modalUrl.value = '';
|
||||
modalActive.checked = true;
|
||||
modalLabel.focus();
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
function updateOrder() {
|
||||
const items = menuList.querySelectorAll('.nav-item');
|
||||
items.forEach((item, index) => {
|
||||
const sortInput = item.querySelector('[data-sort-input]');
|
||||
if (sortInput) {
|
||||
sortInput.value = String(index + 1);
|
||||
}
|
||||
});
|
||||
menuEmpty.style.display = items.length ? 'none' : 'block';
|
||||
}
|
||||
|
||||
function addStagedLink(label, url, isActive) {
|
||||
if (!label || !url) {
|
||||
return;
|
||||
}
|
||||
stagedIndex += 1;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'nav-item';
|
||||
row.setAttribute('draggable', 'true');
|
||||
|
||||
const handle = document.createElement('div');
|
||||
handle.className = 'drag-handle';
|
||||
handle.textContent = '||';
|
||||
handle.title = 'Drag to reorder';
|
||||
|
||||
const labelInput = document.createElement('input');
|
||||
labelInput.className = 'input';
|
||||
labelInput.name = `new[${stagedIndex}][label]`;
|
||||
labelInput.value = label;
|
||||
|
||||
const urlInput = document.createElement('input');
|
||||
urlInput.className = 'input';
|
||||
urlInput.name = `new[${stagedIndex}][url]`;
|
||||
urlInput.value = url;
|
||||
|
||||
const orderInputEl = document.createElement('input');
|
||||
orderInputEl.className = 'input';
|
||||
orderInputEl.name = `new[${stagedIndex}][sort_order]`;
|
||||
orderInputEl.value = '0';
|
||||
orderInputEl.setAttribute('data-sort-input', '');
|
||||
orderInputEl.readOnly = true;
|
||||
|
||||
const activeLabel = document.createElement('label');
|
||||
activeLabel.style.display = 'flex';
|
||||
activeLabel.style.justifyContent = 'center';
|
||||
const activeCheckbox = document.createElement('input');
|
||||
activeCheckbox.type = 'checkbox';
|
||||
activeCheckbox.name = `new[${stagedIndex}][is_active]`;
|
||||
activeCheckbox.value = '1';
|
||||
activeCheckbox.checked = !!isActive;
|
||||
activeLabel.appendChild(activeCheckbox);
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'btn';
|
||||
removeButton.textContent = 'Remove';
|
||||
removeButton.style.padding = '6px 12px';
|
||||
removeButton.style.background = 'transparent';
|
||||
removeButton.style.color = 'var(--text)';
|
||||
removeButton.style.border = '1px solid rgba(255,255,255,0.2)';
|
||||
|
||||
row.appendChild(handle);
|
||||
row.appendChild(labelInput);
|
||||
row.appendChild(urlInput);
|
||||
row.appendChild(orderInputEl);
|
||||
row.appendChild(activeLabel);
|
||||
row.appendChild(removeButton);
|
||||
|
||||
removeButton.addEventListener('click', () => {
|
||||
row.remove();
|
||||
updateOrder();
|
||||
});
|
||||
|
||||
menuList.appendChild(row);
|
||||
enableDrag(row);
|
||||
updateOrder();
|
||||
}
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const selected = pageSelect.options[pageSelect.selectedIndex];
|
||||
if (!selected || !selected.value) {
|
||||
return;
|
||||
}
|
||||
if (selected.value === 'custom') {
|
||||
showModal();
|
||||
return;
|
||||
}
|
||||
const label = selected.getAttribute('data-label') || '';
|
||||
const url = selected.getAttribute('data-url') || '';
|
||||
const isActive = activeInput.checked;
|
||||
addStagedLink(label, url, isActive);
|
||||
pageSelect.value = '';
|
||||
activeInput.checked = true;
|
||||
});
|
||||
|
||||
modalAdd.addEventListener('click', () => {
|
||||
const label = modalLabel.value.trim();
|
||||
const url = modalUrl.value.trim();
|
||||
const isActive = modalActive.checked;
|
||||
addStagedLink(label, url, isActive);
|
||||
hideModal();
|
||||
});
|
||||
|
||||
modalCancel.addEventListener('click', hideModal);
|
||||
modal.addEventListener('click', (event) => {
|
||||
if (event.target === modal) {
|
||||
hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
function getDragAfterElement(container, y) {
|
||||
const elements = [...container.querySelectorAll('.nav-item:not(.dragging)')];
|
||||
return elements.reduce((closest, child) => {
|
||||
const box = child.getBoundingClientRect();
|
||||
const offset = y - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) {
|
||||
return { offset, element: child };
|
||||
}
|
||||
return closest;
|
||||
}, { offset: Number.NEGATIVE_INFINITY }).element;
|
||||
}
|
||||
|
||||
function enableDrag(item) {
|
||||
item.addEventListener('dragstart', () => {
|
||||
item.classList.add('dragging');
|
||||
});
|
||||
item.addEventListener('dragend', () => {
|
||||
item.classList.remove('dragging');
|
||||
updateOrder();
|
||||
});
|
||||
}
|
||||
|
||||
menuList.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
const dragging = menuList.querySelector('.dragging');
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
const afterElement = getDragAfterElement(menuList, event.clientY);
|
||||
if (afterElement == null) {
|
||||
menuList.appendChild(dragging);
|
||||
} else {
|
||||
menuList.insertBefore(dragging, afterElement);
|
||||
}
|
||||
});
|
||||
|
||||
menuList.querySelectorAll('.nav-item').forEach(enableDrag);
|
||||
updateOrder();
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
$content = ob_get_clean();
|
||||
require __DIR__ . '/layout.php';
|
||||
Reference in New Issue
Block a user