Ajouter les pages personnalisées au site public

This commit is contained in:
Alexandre NOEL 2026-10-12 10:42:59 +02:00
commit 21ee1bec36
17 changed files with 428 additions and 65 deletions

View file

@ -31,10 +31,10 @@
<div class="col-lg-6">
<section class="card h-100">
<div class="card-body">
<span class="badge bg-blue-lt mb-2">1.0.0-rc.33</span>
<h3>Un site public mieux connecté</h3>
<p class="text-muted">Les réseaux sociaux renseignés par l’association rejoignent la navigation,
avec des liens discrets, sécurisés et accessibles.</p>
<span class="badge bg-blue-lt mb-2">1.0.0-rc.34</span>
<h3>Un site public qui s’adapte au refuge</h3>
<p class="text-muted">Pages personnalisées, contenus longs, panneaux repliables et ordre du menu
réorganisable rendent chaque site aussi simple ou aussi complet que nécessaire.</p>
</div>
</section>
</div>

View file

@ -9,6 +9,9 @@ $requestPath = rtrim((string)(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_
$activePage = match($requestPath) {
'/','/site' => 'home','/animaux','/site/animaux','/adoption','/site/animal' => 'animals','/presentation','/site/presentation' => 'about','/dons','/site/dons' => 'donation','/contact','/site/contact' => 'contact',default => ''
};
$activeCustomSlug = '';
if (preg_match('#^/site/page/([a-z0-9-]+)$#', $requestPath, $match)) $activeCustomSlug = $match[1];
elseif (PublicSiteController::usesRoot() && preg_match('#^/([a-z0-9-]+)$#', $requestPath, $match)) $activeCustomSlug = $match[1];
$activeSpecies = (string)($selectedSpecies ?? ($animal['species'] ?? ''));
$navClass = static fn (bool$active): string => $active ? ' class="active" aria-current="page"' : '';
$fullAddress = implode(', ', array_filter([$settings['association_address'] ?? '',trim(($settings['association_postal_code'] ?? '') . ' ' . ($settings['association_city'] ?? ''))]));
@ -18,6 +21,16 @@ $socialNetworks = [
'tiktok' => ['TikTok','♪'], 'whatsapp' => ['WhatsApp','☎'], 'linkedin' => ['LinkedIn','in'],
'x' => ['X','𝕏'], 'snapchat' => ['Snapchat','👻'],
];
$customPagesBySlug = [];
foreach (($publicPages ?? []) as $publicPage) $customPagesBySlug[$publicPage['slug']] = $publicPage;
$navigationOrder = json_decode((string)($settings['public_site_navigation_order'] ?? '[]'), true);
$navigationOrder = is_array($navigationOrder) ? $navigationOrder : [];
foreach (['home','animals','about','donation','contact'] as $defaultNavigationItem) {
if (!in_array($defaultNavigationItem, $navigationOrder, true)) $navigationOrder[] = $defaultNavigationItem;
}
foreach (array_keys($customPagesBySlug) as $slug) {
if (!in_array('custom:' . $slug, $navigationOrder, true)) $navigationOrder[] = 'custom:' . $slug;
}
if (!function_exists('public_age')) {
function public_age(array$a): string
{
@ -514,17 +527,18 @@ if (!function_exists('public_age')) {
href="<?= h($siteUrl()) ?>"><?php if (AssociationBrand::path()): ?><img src="<?= h($siteUrl('logo')) ?>"
alt=""><?php endif; ?><span><?= h($siteName) ?></span></a>
<nav class="nav" aria-label="<?= h(t('public_site.navigation')) ?>">
<a<?= $navClass($activePage === 'home') ?> href="<?= h($siteUrl()) ?>">
<?= h(t('public_site.home')) ?></a><?php foreach ($publicSpecies as $species): ?>
<a<?= $navClass($activePage === 'animals' && $activeSpecies === $species['code']) ?>
href="<?= h($siteUrl('animals', ['espece' => $species['code']])) ?>">
<?= h(($species['icon'] ?: '🐾') . ' ' . $species['name']) ?></a><?php endforeach; ?><?php if (($settings['public_site_about_enabled'] ?? '0') === '1'): ?>
<a<?= $navClass($activePage === 'about') ?> href="<?= h($siteUrl('about')) ?>">
<?= h($settings['public_site_about_title']) ?></a><?php endif; ?>
<a<?= $navClass($activePage === 'donation') ?> href="<?= h($siteUrl('donation')) ?>">
<?= h(t('public_site.donation')) ?></a>
<a<?= $navClass($activePage === 'contact') ?> href="<?= h($siteUrl('contact')) ?>">
<?= h(t('public_site.contact')) ?></a><?php foreach ($socialNetworks as $network => [$label,$icon]):
<?php foreach ($navigationOrder as $navigationItem): ?>
<?php if ($navigationItem === 'home'): ?><a<?= $navClass($activePage === 'home') ?> href="<?= h($siteUrl()) ?>"><?= h(t('public_site.home')) ?></a>
<?php elseif ($navigationItem === 'animals'): foreach ($publicSpecies as $species): ?><a<?= $navClass($activePage === 'animals' && $activeSpecies === $species['code']) ?> href="<?= h($siteUrl('animals', ['espece' => $species['code']])) ?>"><?= h(($species['icon'] ?: '🐾') . ' ' . $species['name']) ?></a><?php endforeach; ?>
<?php elseif ($navigationItem === 'about' && ($settings['public_site_about_enabled'] ?? '0') === '1'): ?><a<?= $navClass($activePage === 'about') ?> href="<?= h($siteUrl('about')) ?>"><?= h($settings['public_site_about_title']) ?></a>
<?php elseif ($navigationItem === 'donation'): ?><a<?= $navClass($activePage === 'donation') ?> href="<?= h($siteUrl('donation')) ?>"><?= h(t('public_site.donation')) ?></a>
<?php elseif ($navigationItem === 'contact'): ?><a<?= $navClass($activePage === 'contact') ?> href="<?= h($siteUrl('contact')) ?>"><?= h(t('public_site.contact')) ?></a>
<?php elseif (str_starts_with((string)$navigationItem, 'custom:')):
$navigationSlug = substr((string)$navigationItem, 7);
$navigationPage = $customPagesBySlug[$navigationSlug] ?? null;
if ($navigationPage): ?><a<?= $navClass($activeCustomSlug === $navigationSlug) ?> href="<?= h($siteUrl('custom', ['slug' => $navigationSlug])) ?>"><?= h($navigationPage['title']) ?></a><?php endif; endif; ?>
<?php endforeach; ?>
<?php foreach ($socialNetworks as $network => [$label,$icon]):
$socialUrl = trim((string)($settings['public_site_' . $network . '_url'] ?? ''));
if ($socialUrl === '') continue; ?>
<a class="nav-social-link nav-social-<?= h($network) ?> social-<?= h($network) ?>"

View file

@ -28,6 +28,7 @@
</div>
</div>
<form method="post" action="/settings/public-site/save"><input type="hidden" name="csrf" value="<?= h(Auth::csrf()) ?>">
<input type="hidden" id="public-site-navigation-order" name="public_site_navigation_order" value="<?= h($settings['public_site_navigation_order'] ?? '') ?>">
<div class="card mb-3">
<div class="card-status-top bg-green"></div>
<div class="card-header">
@ -78,10 +79,17 @@
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<div><h3 class="mb-0"><?= h(t('public_site.site_pages')) ?></h3><div class="text-muted small"><?= h(t('public_site.site_pages_help')) ?></div></div>
<button class="btn btn-outline-primary" type="button" id="public-page-add">+ <?= h(t('public_site.add_page')) ?></button>
</div>
<div id="public-pages-sortable" class="mb-3">
<details class="card mb-2 public-page-panel public-sortable-page" data-page-key="home" draggable="true" open>
<summary class="card-header gap-2">
<span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span>
<h3 class="card-title">🏠 <?= h(t('public_site.home')) ?></h3>
</div>
<span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button><span class="public-page-chevron" aria-hidden="true">⌄</span></span>
</summary>
<div class="card-body row g-3">
<div class="col-12"><label class="form-label"><?= h(t('common.title')) ?></label><input class="form-control"
name="public_site_home_title" value="<?= h($settings['public_site_home_title']) ?>" required></div>
@ -89,11 +97,14 @@
class="form-control public-rich-text" name="public_site_home_text"
rows="4"><?= h($settings['public_site_home_text']) ?></textarea></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
</details>
<div class="card mb-2 public-sortable-page" data-page-key="animals" draggable="true"><div class="card-header gap-2"><span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span><h3 class="card-title">🐾 <?= h(t('public_site.animals')) ?></h3><span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button></span></div></div>
<details class="card mb-2 public-page-panel public-sortable-page" data-page-key="about" draggable="true">
<summary class="card-header gap-2">
<span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span>
<h3 class="card-title">📖 <?= h(t('public_site.free_page')) ?></h3>
</div>
<span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button><span class="public-page-chevron" aria-hidden="true">⌄</span></span>
</summary>
<div class="card-body row g-3">
<div class="col-12"><label class="form-check form-switch"><input class="form-check-input" type="checkbox"
name="public_site_about_enabled" value="1"
@ -106,11 +117,13 @@
class="form-control public-rich-text" name="public_site_about_text"
rows="5"><?= h($settings['public_site_about_text']) ?></textarea></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
</details>
<details class="card mb-2 public-page-panel public-sortable-page" data-page-key="donation" draggable="true">
<summary class="card-header gap-2">
<span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span>
<h3 class="card-title">💝 <?= h(t('public_site.donation')) ?></h3>
</div>
<span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button><span class="public-page-chevron" aria-hidden="true">⌄</span></span>
</summary>
<div class="card-body row g-3">
<div class="col-md-4"><label class="form-label"><?= h(t('common.title')) ?></label><input
class="form-control" name="public_site_donation_title"
@ -122,11 +135,13 @@
class="form-control public-rich-text" name="public_site_donation_text"
rows="4"><?= h($settings['public_site_donation_text']) ?></textarea></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">
</details>
<details class="card mb-2 public-page-panel public-sortable-page" data-page-key="contact" draggable="true">
<summary class="card-header gap-2">
<span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span>
<h3 class="card-title">✉️ <?= h(t('public_site.contact')) ?></h3>
</div>
<span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button><span class="public-page-chevron" aria-hidden="true">⌄</span></span>
</summary>
<div class="card-body row g-3">
<div class="col-12">
<div class="alert alert-info py-2"><?= h(t('public_site.contact_reuse')) ?></div><label
@ -143,11 +158,41 @@
value="<?= h($settings['public_site_' . $network . '_url']) ?>" placeholder="https://..."></div>
<?php endforeach; ?>
</div>
</details>
<?php foreach (($customPages ?? []) as $index => $page): ?>
<details class="card mb-2 public-page-panel public-custom-page public-sortable-page" data-page-key="custom:<?= h($page['slug']) ?>" draggable="true">
<summary class="card-header gap-2">
<span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span>
<h3 class="card-title public-page-summary-title"><?= h($page['title']) ?></h3>
<span class="ms-auto d-flex gap-1 public-page-actions">
<button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" title="<?= h(t('public_site.move_up')) ?>" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button>
<button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" title="<?= h(t('public_site.move_down')) ?>" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button>
<span class="public-page-chevron" aria-hidden="true">⌄</span>
</span>
</summary>
<div class="card-body row g-3">
<input type="hidden" name="pages[<?= $index ?>][id]" value="<?= (int) $page['id'] ?>">
<input class="public-page-position" type="hidden" name="pages[<?= $index ?>][position]" value="<?= $index ?>">
<div class="col-12"><label class="form-check form-switch"><input class="form-check-input" type="checkbox" name="pages[<?= $index ?>][enabled]" value="1" <?= (int) $page['enabled'] === 1 ? 'checked' : '' ?>><span class="form-check-label"><?= h(t('public_site.page_published')) ?></span></label></div>
<div class="col-md-7"><label class="form-label"><?= h(t('public_site.page_title')) ?></label><input class="form-control public-page-title" name="pages[<?= $index ?>][title]" value="<?= h($page['title']) ?>" required></div>
<div class="col-md-5"><label class="form-label"><?= h(t('public_site.page_slug')) ?></label><input class="form-control public-page-slug" name="pages[<?= $index ?>][slug]" value="<?= h($page['slug']) ?>" required><div class="form-hint"><?= h(t('public_site.page_slug_help')) ?></div></div>
<div class="col-12"><label class="form-label"><?= h(t('common.text')) ?></label><textarea class="form-control public-rich-text" name="pages[<?= $index ?>][content]" rows="8"><?= h($page['content']) ?></textarea></div>
<div class="col-12"><label class="form-check text-danger"><input class="form-check-input" type="checkbox" name="pages[<?= $index ?>][delete]" value="1"><span class="form-check-label"><?= h(t('public_site.delete_page')) ?></span></label></div>
</div>
</details>
<?php endforeach; ?>
</div>
<div class="card mb-3">
<div class="card-header">
<template id="public-page-template">
<details class="card mb-2 public-page-panel public-custom-page public-sortable-page" data-page-key="custom:" draggable="true" open>
<summary class="card-header gap-2"><span class="public-page-drag" title="<?= h(t('public_site.drag_page')) ?>" aria-hidden="true">⠿</span><h3 class="card-title public-page-summary-title"><?= h(t('public_site.new_page')) ?></h3><span class="ms-auto d-flex gap-1 public-page-actions"><button class="btn btn-sm btn-icon btn-outline-secondary public-page-up" type="button" aria-label="<?= h(t('public_site.move_up')) ?>">↑</button><button class="btn btn-sm btn-icon btn-outline-secondary public-page-down" type="button" aria-label="<?= h(t('public_site.move_down')) ?>">↓</button><span class="public-page-chevron" aria-hidden="true">⌄</span></span></summary>
<div class="card-body row g-3"><input class="public-page-position" type="hidden" name="pages[__INDEX__][position]" value="0"><div class="col-12"><label class="form-check form-switch"><input class="form-check-input" type="checkbox" name="pages[__INDEX__][enabled]" value="1" checked><span class="form-check-label"><?= h(t('public_site.page_published')) ?></span></label></div><div class="col-md-7"><label class="form-label"><?= h(t('public_site.page_title')) ?></label><input class="form-control public-page-title" name="pages[__INDEX__][title]" required></div><div class="col-md-5"><label class="form-label"><?= h(t('public_site.page_slug')) ?></label><input class="form-control public-page-slug" name="pages[__INDEX__][slug]" required><div class="form-hint"><?= h(t('public_site.page_slug_help')) ?></div></div><div class="col-12"><label class="form-label"><?= h(t('common.text')) ?></label><textarea class="form-control public-rich-text" name="pages[__INDEX__][content]" rows="8"></textarea></div><div class="col-12"><button class="btn btn-outline-danger public-page-remove" type="button"><?= h(t('public_site.delete_page')) ?></button></div></div>
</details>
</template>
<details class="card mb-3 public-page-panel">
<summary class="card-header">
<h3 class="card-title">⚖️ <?= h(t('public_site.legal_privacy')) ?></h3>
</div>
<span class="public-page-chevron" aria-hidden="true">⌄</span>
</summary>
<div class="card-body row g-3">
<div class="col-md-6"><label class="form-label"><?= h(t('public_site.legal')) ?></label><textarea
class="form-control" name="public_site_legal_text"
@ -157,9 +202,12 @@
rows="16"><?= h(PublicSiteController::privacyText($settings)) ?></textarea></div>
<div class="col-12 form-hint"><?= h(t('public_site.legal_hint')) ?></div>
</div>
</div>
</details>
<div class="text-end mb-4"><button class="btn btn-primary"><?= h(t('common.save')) ?></button></div>
</form>
<style>
.public-page-panel>summary{cursor:pointer;display:flex;align-items:center;list-style:none}.public-page-panel>summary::-webkit-details-marker{display:none}.public-page-chevron{display:inline-flex;transition:transform .2s;font-size:1.25rem}.public-page-panel[open]>summary .public-page-chevron{transform:rotate(180deg)}.public-page-drag{cursor:grab;font-size:1.35rem;color:var(--tblr-secondary);padding:.25rem}.public-custom-page.is-dragging{opacity:.45}.public-custom-page.drag-over{outline:2px dashed var(--tblr-primary);outline-offset:2px}.public-page-actions button{position:relative;z-index:2}
</style>
<script>
let activePublicEditor = null;
const imageLibrary = document.getElementById('public-image-library');
@ -173,15 +221,20 @@ const insertImage = (button) => {
textarea.setRangeText(`![${safeAlt}](${button.dataset.url})\n`, textarea.selectionStart, textarea.selectionEnd, 'end');
textarea.focus();
};
document.querySelectorAll('textarea').forEach((textarea) => {
const enhanceTextarea = (textarea) => {
if (textarea.dataset.autoResize === '1') return;
textarea.dataset.autoResize = '1';
const resize = () => {
textarea.style.height = 'auto';
textarea.style.height = Math.max(140, textarea.scrollHeight + 8) + 'px';
};
textarea.addEventListener('input', resize);
resize();
});
document.querySelectorAll('.public-rich-text').forEach((textarea) => {
};
const enhancePublicEditor = (textarea) => {
enhanceTextarea(textarea);
if (textarea.dataset.richEditor === '1') return;
textarea.dataset.richEditor = '1';
textarea.addEventListener('focus', () => { activePublicEditor = textarea; });
const toolbar = document.createElement('div');
toolbar.className = 'btn-list mb-2';
@ -211,7 +264,9 @@ document.querySelectorAll('.public-rich-text').forEach((textarea) => {
});
toolbar.append(imageButton);
textarea.before(toolbar);
});
};
document.querySelectorAll('textarea').forEach(enhanceTextarea);
document.querySelectorAll('.public-rich-text').forEach(enhancePublicEditor);
document.querySelectorAll('.public-image-insert').forEach((button) => {
button.addEventListener('click', () => insertImage(button));
});
@ -243,4 +298,86 @@ document.getElementById('public-image-upload').addEventListener('submit', async
submit.disabled = false;
}
});
const pagesContainer = document.getElementById('public-pages-sortable');
const navigationOrderInput = document.getElementById('public-site-navigation-order');
let publicPageIndex = <?= (int) count($customPages ?? []) ?>;
const refreshPagePositions = () => {
const pages = [...pagesContainer.querySelectorAll('.public-sortable-page')];
pages.forEach((page, position) => {
const input = page.querySelector('.public-page-position');
if (input) input.value = String(position);
page.querySelector('.public-page-up')?.toggleAttribute('disabled', position === 0);
page.querySelector('.public-page-down')?.toggleAttribute('disabled', position === pages.length - 1);
});
navigationOrderInput.value = JSON.stringify(pages.map((page) => page.dataset.pageKey).filter((key) => key && key !== 'custom:'));
};
const slugify = (value) => value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
const wirePage = (page) => {
const title = page.querySelector('.public-page-title');
const slug = page.querySelector('.public-page-slug');
let slugEdited = Boolean(slug?.value);
slug?.addEventListener('input', () => { slugEdited = Boolean(slug.value); });
slug?.addEventListener('input', () => { page.dataset.pageKey = 'custom:' + slugify(slug.value); refreshPagePositions(); });
title?.addEventListener('input', () => {
page.querySelector('.public-page-summary-title').textContent = title.value.trim() || <?= json_encode(t('public_site.new_page'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
if (!slugEdited && slug) {
slug.value = slugify(title.value);
page.dataset.pageKey = 'custom:' + slug.value;
refreshPagePositions();
}
});
page.querySelectorAll('.public-page-actions button').forEach((button) => button.addEventListener('click', (event) => event.stopPropagation()));
page.querySelectorAll('input,textarea,button').forEach((control) => control.addEventListener('mousedown', () => { page.draggable = false; }));
page.querySelector('.public-page-drag')?.addEventListener('mousedown', () => { page.draggable = true; });
page.querySelector('.public-page-up')?.addEventListener('click', () => {
if (page.previousElementSibling) pagesContainer.insertBefore(page, page.previousElementSibling);
refreshPagePositions();
});
page.querySelector('.public-page-down')?.addEventListener('click', () => {
if (page.nextElementSibling) pagesContainer.insertBefore(page.nextElementSibling, page);
refreshPagePositions();
});
page.querySelector('.public-page-remove')?.addEventListener('click', () => { page.remove(); refreshPagePositions(); });
page.querySelectorAll('textarea').forEach(enhanceTextarea);
page.querySelectorAll('.public-rich-text').forEach(enhancePublicEditor);
page.addEventListener('dragstart', (event) => {
if (!event.target.closest('.public-page-drag') && document.activeElement !== page.querySelector('.public-page-drag')) {
// The whole card remains draggable for browser compatibility.
}
page.classList.add('is-dragging');
event.dataTransfer.effectAllowed = 'move';
});
page.addEventListener('dragend', () => {
page.draggable = true;
page.classList.remove('is-dragging');
pagesContainer.querySelectorAll('.drag-over').forEach((item) => item.classList.remove('drag-over'));
refreshPagePositions();
});
};
let savedNavigationOrder = [];
try { savedNavigationOrder = JSON.parse(navigationOrderInput.value || '[]'); } catch (_) {}
const initialPages = [...pagesContainer.querySelectorAll('.public-sortable-page')];
const orderedPages = savedNavigationOrder.map((key) => initialPages.find((item) => item.dataset.pageKey === key)).filter(Boolean);
[...orderedPages, ...initialPages.filter((item) => !orderedPages.includes(item))].forEach((page) => pagesContainer.append(page));
pagesContainer.querySelectorAll('.public-sortable-page').forEach(wirePage);
pagesContainer.addEventListener('dragover', (event) => {
event.preventDefault();
const dragging = pagesContainer.querySelector('.is-dragging');
const target = event.target.closest('.public-sortable-page');
if (!dragging || !target || dragging === target) return;
pagesContainer.querySelectorAll('.drag-over').forEach((item) => item.classList.remove('drag-over'));
target.classList.add('drag-over');
const box = target.getBoundingClientRect();
pagesContainer.insertBefore(dragging, event.clientY < box.top + box.height / 2 ? target : target.nextElementSibling);
});
document.getElementById('public-page-add').addEventListener('click', () => {
const html = document.getElementById('public-page-template').innerHTML.replaceAll('__INDEX__', String(publicPageIndex++));
pagesContainer.insertAdjacentHTML('beforeend', html);
const page = pagesContainer.lastElementChild;
wirePage(page);
refreshPagePositions();
page.querySelector('.public-page-title').focus();
});
refreshPagePositions();
</script>