Siteβ―Builder
Editing:
nav-builde1r.php
writable 0666
<?php /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ NavβMaker 1.2 β publicβroot edition Scans the main document root for maiβ* projects, regardless of where this file itself lives. βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ /* βΌ Optional security */ require_once $_SERVER['DOCUMENT_ROOT'] . '/members/lib/auth.php'; require_login(); // must be loggedβin if (current_user()['role'] !== 'admin') { forbidden_page(); // only admins allowed } /* ----- CONFIG ----- */ define('ROOT_PATTERN', '/^mai-[a-z0-9\-]+$/'); // lowerβcase prefix rule define('MAX_DEPTH', 6); // folder recursion define('ALLOW_EXT', '/\.(html?|php|md|txt|json)$/i'); /* ----- locate PUBLIC ROOT ----- */ $PUBLIC_ROOT = realpath($_SERVER['DOCUMENT_ROOT']) ?: __DIR__; /* ----- gather candidate site roots (maiβ*) ----- */ $allRoots = array_map('basename', glob($PUBLIC_ROOT.'/mai-*', GLOB_ONLYDIR|GLOB_NOSORT)); /* legacy fallback */ if (is_dir($PUBLIC_ROOT.'/site')) $allRoots[] = 'site'; /* autoβselect: maiβfolder the script resides in (if any) */ $autoDefault = ''; for ($p = __DIR__; $p && $p !== $PUBLIC_ROOT; $p = dirname($p)) { $b = basename($p); if (preg_match(ROOT_PATTERN, $b)) { $autoDefault = $b; break; } } sort($allRoots, SORT_NATURAL | SORT_FLAG_CASE); /* ----- request params ----- */ $rootName = $_GET['root'] ?? $_POST['root'] ?? ($autoDefault ?: ($allRoots[0] ?? '')); $rootPath = realpath($PUBLIC_ROOT.'/'.$rootName) ?: $PUBLIC_ROOT; // safety $menu = $_GET['menu'] ?? $_POST['menu'] ?? ''; $action = $_POST['action'] ?? ''; /* ---------- helpers ---------- */ function listTree(string $base, int $depth=0): string{ if ($depth > MAX_DEPTH) return ''; $out=''; foreach (scandir($base) as $it){ if ($it[0]==='.') continue; $full="$base/$it"; if (is_dir($full)){ $out .= '<details><summary>'.htmlspecialchars($it).'</summary>' . listTree($full, $depth+1).'</details>'; } elseif (preg_match(ALLOW_EXT,$it)){ $rel = substr($full, strlen(realpath($GLOBALS['rootPath']))+1); $out .= '<label class="file"><input type="checkbox" data-path="'.htmlspecialchars($rel).'"> ' . htmlspecialchars($it).'</label>'; } } return $out; } function navDir(): string{ $p = $GLOBALS['rootPath'].'/parts/nav'; if (!is_dir($p)) mkdir($p,0755,true); return $p; } function navFile(string $menu): string{ return navDir().'/'.preg_replace('/[^a-z0-9_\-]/i','', $menu).'.json'; } /* ---------- AJAX save ---------- */ if ($action==='save'){ file_put_contents(navFile($menu), $_POST['json'] ?? '[]'); exit('ok'); } /* ---------- load existing ---------- */ $items=[]; if ($menu && file_exists(navFile($menu))) $items = json_decode(file_get_contents(navFile($menu)), true) ?? []; /* ---------- UI ---------- */ ?><!DOCTYPE html><html lang="en"><head><meta charset="utf-8"> <title>NavβMaker β <?=htmlspecialchars($rootName)?></title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root{--bg:#101119;--panel:#1c1d27;--accent:#ff4ecd;--fg:#e8e8ee;--radius:12px; font-family:system-ui,sans-serif;color:var(--fg);} body{margin:0;background:var(--bg);display:flex;flex-direction:column;min-height:100vh} h1{font-size:1.5rem;padding:1rem 2rem;margin:0;background:var(--panel)} main{flex:1;display:grid;grid-template-columns:320px 1fr;gap:2rem;padding:2rem} section{background:var(--panel);padding:1.2rem;border-radius:var(--radius);overflow:auto} label.file{display:block;margin:.25rem 0} #builder li{list-style:none;background:#2b2c3b;margin:.3rem 0;padding:.5rem;border-radius:8px; display:flex;gap:.6rem;align-items:center} #builder input[type=text]{flex:1 1 auto;border:none;background:transparent;color:inherit} #builder input[type=text]:focus{outline:1px solid var(--accent)} #builder button{background:var(--accent);color:#fff;border:none;padding:.3rem .6rem;border-radius:6px;cursor:pointer} #builder li.dragging{opacity:.4} button.big{padding:.8rem 1.6rem;font-weight:600;border-radius:40px;background:var(--accent); border:none;color:#fff;font-size:1rem;cursor:pointer;margin-top:1rem;width:100%} </style></head><body> <h1>NavβMaker <span style="opacity:.6;font-size:.9rem">Β·Β <?=htmlspecialchars($rootName)?> <?= $menu ? 'Β /Β '.htmlspecialchars($menu) : '' ?></span> </h1> <main> <!-- LEFT PANE --> <section> <h2 style="margin-top:0;font-size:1.15rem">0β―Β·β―Choose SiteβRoot</h2> <form method="get"> <select name="root" style="width:100%;padding:.5rem;border-radius:8px;margin-bottom:1rem"> <?php foreach ($allRoots as $r){ echo '<option'.($r===$rootName?' selected':'').'>'.htmlspecialchars($r).'</option>'; } ?> </select> <button class="big">Open Root</button> </form> <?php if($rootName): ?> <hr style="border:none;border-top:1px solid #333;margin:1.3rem 0"> <h2 style="font-size:1.1rem">1β―Β·β―Menu Name</h2> <form method="get"> <input type="hidden" name="root" value="<?=htmlspecialchars($rootName)?>"> <input name="menu" placeholder="primary, footerβ¦" value="<?=htmlspecialchars($menu)?>" style="width:100%;padding:.5rem;border-radius:8px;margin-bottom:1rem"> <button class="big">Create / Load</button> </form> <?php endif; ?> <?php if($menu): ?> <hr style="border:none;border-top:1px solid #333;margin:1.3rem 0"> <h2 style="font-size:1.1rem">2β―Β·β―Pick Pages</h2> <div style="max-height:45vh;overflow:auto"> <?= listTree($rootPath) ?> </div> <?php endif; ?> </section> <?php if($menu): ?> <!-- RIGHT PANE --> <section> <h2 style="margin-top:0;font-size:1.2rem">3β―Β·β―Arrange & Edit</h2> <ul id="builder"> <?php foreach($items as $i): ?> <li draggable="true"><span class="drag">β</span> <input type="text" class="label" value="<?=htmlspecialchars($i['label'])?>"> <input type="text" class="path" value="<?=htmlspecialchars($i['path'])?>"> <label><input type="checkbox" class="active" <?=!empty($i['active'])?'checked':''?>>active</label> <button class="del">β</button> </li> <?php endforeach; ?> </ul> <button id="save" class="big">Save Menu</button> <p id="status" style="opacity:.7;margin-top:.8rem"></p> <template id="tmpl"> <li draggable="true"><span class="drag">β</span> <input type="text" class="label" value=""> <input type="text" class="path" value=""> <label><input type="checkbox" class="active" checked>active</label> <button class="del">β</button> </li> </template> </section> <?php endif; ?> </main> <?php if($menu): ?> <script> const tmpl=document.getElementById('tmpl').content; const ul =document.getElementById('builder'); /* add from fileβtree */ document.querySelectorAll('label.file input[type=checkbox]').forEach(cb=>{ cb.addEventListener('change',()=>{ if(!cb.checked) return; const li=tmpl.cloneNode(true); li.querySelector('.label').value=cb.parentNode.textContent.trim(); li.querySelector('.path').value =cb.dataset.path; ul.append(li); }); }); /* delete */ ul.addEventListener('click',e=>{ if(e.target.classList.contains('del')) e.target.closest('li').remove(); }); /* dragβreorder */ let drag=null; ul.addEventListener('dragstart',e=>{drag=e.target;drag.classList.add('dragging');}); ul.addEventListener('dragend',()=>drag&&drag.classList.remove('dragging')); ul.addEventListener('dragover',e=>{ e.preventDefault(); const after=[...ul.querySelectorAll('li:not(.dragging)')] .find(li=>e.clientY<li.getBoundingClientRect().top+li.offsetHeight/2); ul.insertBefore(drag, after); }); /* save */ document.getElementById('save').onclick=()=>{ const data=[...ul.children].map(li=>({ label : li.querySelector('.label').value.trim(), path : li.querySelector('.path').value.trim(), active: li.querySelector('.active').checked })); fetch('',{ method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body:new URLSearchParams({ action:'save', root:'<?=htmlspecialchars($rootName)?>', menu:'<?=htmlspecialchars($menu)?>', json:JSON.stringify(data) }) }) .then(r=>r.text()) .then(t=>document.getElementById('status').textContent = t==='ok' ? 'β Saved' : 'Error'); }; </script> <?php endif; ?> </body></html>
Save changes
Create folder
writable 0777
Create
Cancel