Siteβ―Builder
Editing:
modules.php
writable 0666
<?php /************************************************************************* * USER MODULE CUSTOMISER β v1.5 (dragβtoβreorder) *************************************************************************/ require_once __DIR__.'/lib/auth.php'; session_start(); require_login(); /* ββ helpers ββ */ function jsonc_load(string $file): array { if (!is_readable($file)) return []; $txt=file_get_contents($file); $txt=preg_replace('!//.*?$!m','',$txt); $txt=preg_replace('!/\\*.*?\\*/!s','',$txt); return json_decode($txt,true)?:[]; } function safe_write(string $f,string $t):void{ $tmp=$f.'.tmp'; if(file_put_contents($tmp,$t,LOCK_EX)===false)throw new RuntimeException("write $tmp"); if(!rename($tmp,$f)){@unlink($tmp);throw new RuntimeException("rename $tmp");} } /* ββ paths ββ */ $user=current_user(); $slug=strtolower($user['site_slug']?:$user['username']); $isBiz=preg_match('/^\d{10}$/',$slug); $root=rtrim($_SERVER['DOCUMENT_ROOT'],'/'); $profileDir=$isBiz?"$root/ph/$slug/":"$root/social/$slug/"; $cfgFile=$profileDir.'config.json'; $profFile=$profileDir.'profile.json'; $masterFile="$root/pages/modules/modules.json"; /* ββ load ββ */ $cfg=jsonc_load($cfgFile); /* manual profile loader so we get raw values */ $profile = is_file($profFile) ? json_decode(file_get_contents($profFile), true) : []; if (!is_array($profile)) $profile = []; $master=jsonc_load($masterFile); /* hero text */ $heroHeading=$profile['display_name']??$profile['name']??'Welcome!'; $heroSub=$profile['slogan']??'Powered by BestDealOn'; $cfg['hero']['heading']=$heroHeading; $cfg['hero']['sub']=$heroSub; $cfg['hero']['bg']=$cfg['hero']['bg']??'#0066ff'; $cfg['hero']['fg']=$cfg['hero']['fg']??'#ffffff'; $cfg['site_name']??=($profile['display_name']??'BestDealOn microsite'); $cfg['tier']??='free'; /* master catalogue + admin locks */ $catalog=[];$adminActive=[]; foreach($master as $row){ $name=$row['name']; $catalog[]=$name; $adminActive[$name]=!empty($row['active']); } /* current ON/OFF + current ORDER (use array order if present) */ $currentON=[];$currentOrder=[]; if(isset($cfg['modules'])){ foreach($cfg['modules'] as $pos=>$row){ $n=$row['name'];$currentON[$n]=!empty($row['active']);$currentOrder[$n]=$pos; } } foreach($catalog as $n){$currentON+=[ $n=>false ]; $currentOrder+=[ $n=>PHP_INT_MAX ];} /* csrf */ $csrf=$_SESSION['csrf']??=bin2hex(random_bytes(16)); /* ββ save handler ββ */ if($_SERVER['REQUEST_METHOD']==='POST' && ($_POST['csrf']??'')===$csrf){ try{ $in=json_decode($_POST['json']??'',true,512,JSON_THROW_ON_ERROR); if(!isset($in['order'],$in['modules']))throw new RuntimeException('Bad JSON'); /* hero colours */ foreach(['bg','fg'] as $k) if(isset($in['hero'][$k])) $cfg['hero'][$k]=$in['hero'][$k]; /* rebuild list in submitted order */ $cfg['modules']=[]; foreach($in['order'] as $name){ if(!in_array($name,$catalog, true)) continue; // ignore rogue $allowed=$adminActive[$name]??true; $want =!empty($in['modules'][$name]); $cfg['modules'][]=[ 'name'=>$name, 'active'=>$allowed&&$want ]; } /* append any stillβmissing modules to preserve full list */ foreach($catalog as $name){ if(!array_filter($cfg['modules'],fn($m)=>$m['name']===$name)){ $cfg['modules'][]=[ 'name'=>$name, 'active'=>false ]; } } /* oneβliner formatting */ $json=json_encode($cfg,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); $lines=array_map(fn($m)=> sprintf(' { "name": "%s", "active": %s }', $m['name'],$m['active']?'true ':'false'),$cfg['modules']); $block="\"modules\": [\n\n".implode(",\n",$lines)."\n ]"; $json=preg_match('/"modules":/',$json)? preg_replace('/"modules":\s*\[[^\]]*\]/s',$block,$json,1): rtrim($json,"}\n").",\n $block\n}\n"; if(!is_dir($profileDir)&&!@mkdir($profileDir,0775,true)) throw new RuntimeException("mkdir $profileDir"); safe_write($cfgFile,$json); echo'{"ok":1}';exit; }catch(Throwable $e){ http_response_code(400);echo json_encode(['error'=>$e->getMessage()]);exit; } } ?> <!doctype html><html lang="en"><head> <meta charset="utf-8"><title>Module Customiser</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> :root{--off:#f8d3d3;--on:#c8f2d9;--dis:#dedede;--gold:#ffb63b; font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif} body{margin:0;background:#f5f8fb;color:#123;text-align:center;font-size:18px} h1{margin:1.6rem 0 1.1rem;font-size:1.9rem;font-weight:800;color:#0d305d} .pickers{display:flex;flex-wrap:wrap;justify-content:center;gap:1.2rem;margin-bottom:1.8rem} .pickers p{margin:0;font-size:1rem;font-weight:600} .pickers label{font-size:.95rem} .pickers input[type=color]{appearance:none;border:none;width:46px;height:28px;border-radius:6px;cursor:pointer} .grid{display:flex;flex-wrap:wrap;justify-content:center;gap:.8rem;padding:0 1rem;max-width:960px;margin:0 auto} .tile{width:210px;border:3px solid var(--gold);border-radius:22px;padding:1.6rem .8rem 1.2rem; box-shadow:0 4px 18px #dde3fa66;cursor:grab;transition:.14s;background:#fff} .tile:active{cursor:grabbing} .tile.disabled{cursor:not-allowed;opacity:.6} .tile h3{margin:0 0 .9rem;font-size:1.05rem;font-weight:700;color:#0d305d} .pill{display:inline-block;padding:.18em 1.2em;border-radius:999px;font-size:.83rem;font-weight:700} .off{background:var(--off);color:#a10000} .on{background:var(--on);color:#00640f} .disabled .pill{background:var(--dis);color:#666} .debug{font-size:.78rem;color:#555;margin:1.6rem auto;word-break:break-all;max-width:90%} </style> </head><body> <h1>Module Customiser</h1> <form id="form" onsubmit="return false"> <div class="pickers"> <p><strong>Heading:</strong> <?=htmlspecialchars($heroHeading)?></p> <p><strong>Subβtext:</strong> <?=htmlspecialchars($heroSub)?></p> <label>BG Colour:<input type="color" id="bg" value="<?=htmlspecialchars($cfg['hero']['bg'])?>"></label> <label>Text Colour:<input type="color" id="fg" value="<?=htmlspecialchars($cfg['hero']['fg'])?>"></label> </div> <div class="grid" id="grid"> <?php uasort($catalog, fn($a,$b)=>$currentOrder[$a]<=>$currentOrder[$b]); // initial order foreach($catalog as $name): $nice=ucwords(str_replace('-',' ',$name)); $allowed=$adminActive[$name]??true; $on=$allowed && $currentON[$name]; $pillCls=$allowed?($on?'on':'off'):'disabled'; $tileCls=$allowed?'':'disabled'; $txt=$allowed?($on?'ON':'OFF'):'DISABLED';?> <div class="tile <?=$tileCls?>" draggable="<?=$allowed?'true':'false'?>" data-mod="<?=$name?>"> <h3><?=$nice?></h3><span class="pill <?=$pillCls?>"><?=$txt?></span> </div> <?php endforeach;?> </div> <p class="debug">Config path: <code><?=htmlspecialchars($cfgFile)?></code></p> <input type="hidden" id="csrf" value="<?=$csrf?>"> </form> <script> /* helpers */ const $=s=>document.querySelector(s);let tID; const debounce=(fn,ms)=>{clearTimeout(tID);tID=setTimeout(fn,ms);}; /* colour pickers */ ['#bg','#fg'].forEach(sel=>$(sel).addEventListener('input',()=>debounce(save,200))); /* toggle ON/OFF and dragβsort */ const grid=$('#grid'); grid.addEventListener('click',e=>{ const tile=e.target.closest('.tile');if(!tile||tile.classList.contains('disabled'))return; if(e.target.closest('.pill')){ /* click pill -> toggle */ const pill=tile.querySelector('.pill'); pill.classList.toggle('on');pill.classList.toggle('off'); pill.textContent=pill.classList.contains('on')?'ON':'OFF'; debounce(save,200); } }); /* drag */ let dragEl=null; grid.addEventListener('dragstart',e=>{ if(e.target.classList.contains('disabled')){e.preventDefault();return;} dragEl=e.target;e.dataTransfer.effectAllowed='move';}); grid.addEventListener('dragover',e=>{ if(!dragEl)return;const tgt=e.target.closest('.tile');if(!tgt||tgt===dragEl)return; e.preventDefault(); const rect=tgt.getBoundingClientRect(); const swap=(e.clientY - rect.top) / (rect.bottom-rect.top) > .5; grid.insertBefore(dragEl,swap?tgt.nextSibling:tgt); }); grid.addEventListener('dragend',()=>{dragEl=null;save();}); /* save */ function save(){ const order=[...grid.querySelectorAll('.tile')].map(t=>t.dataset.mod); const mods={}; grid.querySelectorAll('.tile').forEach(t=>{ const pill=t.querySelector('.pill'); mods[t.dataset.mod]=pill.classList.contains('on'); }); fetch(location.href,{ method:'POST', body:new URLSearchParams({ csrf:$('#csrf').value, json:JSON.stringify({ hero:{bg:$('#bg').value,fg:$('#fg').value}, order, modules:mods }) }) }).then(r=>r.json()).then(j=>{if(!j.ok)alert('Save failed: '+(j.error||''));}); } </script> </body></html>
Save changes
Create folder
writable 0777
Create
Cancel