Site Builder
Editing:
edit.php
writable 0666
<?php /************************************************************************** * UNIVERSAL FAQ BUILDER – social + business v1.1 * ---------------------------------------------------------------------- * ✎ 2025 BestDealOn. Requires PHP 8.1+ * NEW IN v1.1 ─────────────────────────────────────────────────────────── * • Pencil “✎ Edit” button appears next to the answer when a Q/A is * expanded. Clicking it jumps that entry into the form for editing. **************************************************************************/ /* ---------- auth -------------------------------------------------------- */ require_once $_SERVER['DOCUMENT_ROOT'].'/members/lib/auth.php'; require_login(); $me = current_user(); /* ---------- locate profile --------------------------------------------- */ $root = $_SERVER['DOCUMENT_ROOT']; $phone = preg_replace('/\D/','', $_GET['ph'] ?? ''); $slug = preg_replace('/[^a-z0-9_]/i','', $_GET['user'] ?? ''); if ($phone==='') $phone=null; if ($slug ==='') $slug =null; if (!$phone && !$slug){ http_response_code(400); exit('Missing ?ph= or ?user='); } if ($phone){ if (strlen($phone)!==10) { http_response_code(400); exit('Bad phone'); } $mode = 'biz'; $dir = "$root/ph/$phone"; $profile = "$dir/profile.json"; $niceId = $phone; $pageURL = "https://bestdealon.com/$phone/"; }else{ $mode = 'social'; $dir = "$root/social/$slug"; $profile = "$dir/profile.json"; $niceId = '@'.$slug; $pageURL = "https://bestdealon.com/social/$slug/"; } if(!is_file($profile)) { http_response_code(404); exit('Profile not found'); } $profileData = json_decode(file_get_contents($profile),true); $paid = !empty($profileData['premium']) && strtolower($profileData['premium'])==='on'; $faqFile = "$dir/faq.json"; /* ---------- helpers ----------------------------------------------------- */ function esc($s){ return htmlspecialchars($s??'',ENT_QUOTES,'UTF-8'); } function normalise(array $raw): array { $out=[]; foreach($raw as $cat){ $faqs=[]; foreach($cat['faqs']??[] as $qa){ $faqs[]=[ 'q'=>trim($qa['q']??''), 'a'=>trim($qa['a']??''), 'active'=>($qa['active']??true)?true:false ]; } $out[]=[ 'name'=>trim($cat['name']??''), 'faqs'=>$faqs ]; } return $out; } function loadFAQ(string $f): array { if(!is_file($f)) return []; return normalise(json_decode(file_get_contents($f), true, 512, JSON_THROW_ON_ERROR)); } function saveFAQ(string $f, array $data): void { file_put_contents($f, json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES), LOCK_EX); } /* ---------- raw download ----------------------------------------------- */ if(isset($_GET['download']) && $paid){ header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="faq-'.$niceId.'.json"'); echo json_encode(loadFAQ($faqFile), JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); exit; } /* ---------- API --------------------------------------------------------- */ if(isset($_GET['fetch'])){ echo json_encode([ 'success'=>true, 'data'=>loadFAQ($faqFile), 'paid'=>$paid ]); exit; } if($_SERVER['REQUEST_METHOD']==='POST'){ if(!$paid){ echo '{"success":false,"msg":"Free tier – read‑only"}'; exit; } $in=json_decode(file_get_contents('php://input'),true); /* upload bulk ------------------------------------ */ if(($in['action']??'')==='upload'){ $blob=json_decode($in['blob']??'',true); if(!is_array($blob)){ echo '{"success":false}'; exit; } saveFAQ($faqFile, normalise($blob)); echo '{"success":true}'; exit; } /* other ops need cat+idx ------------------------- */ $cat = intval($in['cat'] ?? 0); $idx = intval($in['idx'] ?? 0); $all = loadFAQ($faqFile); if(!isset($all[$cat])) $all[$cat]=['name'=>'New Category','faqs'=>[]]; switch($in['action']){ case 'saveCat': $all[$cat]['name']=trim($in['name']??''); break; case 'newCat': $all[]=['name'=>trim($in['name']??'New Category'),'faqs'=>[]]; $cat = array_key_last($all); break; case 'deleteCat': array_splice($all,$cat,1); $cat = max(0, $cat-1); break; case 'saveQA': $qa = [ 'q'=>trim($in['q']??''), 'a'=>trim($in['a']??''), 'active'=>true ]; if(isset($all[$cat]['faqs'][$idx])) $all[$cat]['faqs'][$idx]=$qa; else $all[$cat]['faqs'][]=$qa; $idx = array_search($qa,$all[$cat]['faqs'],true); break; case 'deleteQA': array_splice($all[$cat]['faqs'],$idx,1); $idx = max(0,$idx-1); break; case 'moveQA': $to = intval($in['to']??0); if(isset($all[$cat]['faqs'][$idx]) && isset($all[$cat]['faqs'][$to])){ $swap=$all[$cat]['faqs'][$idx]; $all[$cat]['faqs'][$idx]=$all[$cat]['faqs'][$to]; $all[$cat]['faqs'][$to]=$swap; $idx=$to; } break; } saveFAQ($faqFile,$all); echo json_encode(['success'=>true,'cat'=>$cat,'idx'=>$idx]); exit; } /* ---------- HTML -------------------------------------------------------- */ ?><!doctype html><html lang="en"><head> <meta charset="utf-8"><meta name=viewport content="width=device-width,initial-scale=1"> <title>FAQ Builder – <?=esc($niceId)?></title> <style> :root{--bg:#f6f8fb;--fg:#233;--accent:#2357d7;--accent-dk:#1746ba;--gold:#ef8f13} *{box-sizing:border-box;font-family:system-ui,Arial,sans-serif;margin:0} body{background:var(--bg);color:var(--fg)} .top{background:#eee;padding:.8em 1.2em;font-weight:900} .container{max-width:820px;margin:2rem auto;padding:0 1rem} h1{font-size:1.45rem;margin:.2rem 0 1rem} select,input,textarea{width:100%;padding:.55rem;border:1px solid #b7c2df;border-radius:7px} textarea{min-height:140px} label{font-weight:600;margin-top:1rem;display:block} button{padding:.55rem 1.2rem;border:none;border-radius:8px;font-weight:700;cursor:pointer} .primary{background:var(--gold);color:#fff}.primary:hover{filter:brightness(1.08)} .alt{background:#aac8ff;color:#103e9d}.alt:hover{background:#8ab4ff} .danger{background:#ffdddd;color:#c00}.danger:hover{background:#ffcaca} .btn-row{display:flex;flex-wrap:wrap;gap:.7rem;margin-top:1.2rem} .badge{display:inline-block;padding:.3rem .7rem;border-radius:12px;background:#2e7d32;color:#fff;font-size:.78rem;font-weight:700;margin-left:.5rem} .notice{margin-top:1rem;font-weight:600} .form-card{background:#fff;padding:2rem;border-radius:14px;box-shadow:0 4px 14px #0001;border:2px solid #ffd973} .preview-card{background:#fff;padding:1.5rem;border-radius:14px;border:3px solid #ffb63b;box-shadow:0 6px 18px #c7a96a40;margin-top:2rem} .preview-card details{border-bottom:1px solid #f2e2c0;padding:.6rem 0} .preview-card summary{cursor:pointer;font-weight:700;color:#b35c00;list-style:none} .preview-card summary::-webkit-details-marker{display:none} .preview-card .q{font-weight:700;color:#b35c00} .preview-card .a{margin-top:.4rem;line-height:1.55} .edit-btn{display:none;margin-left:.6rem;font-size:.85rem;color:var(--accent);cursor:pointer;font-weight:700} details[open] .edit-btn{display:inline} .edit-btn:hover{color:var(--accent-dk)} .tool{background:#ffeebb;padding:.4rem .9rem;border-radius:8px;margin-left:auto} @media(max-width:580px){.btn-row{flex-direction:column}} </style> </head><body> <div class="top"><a href="/" style="text-decoration:none;color:#2a3ca5">BestDealOn</a> » <a href="/members/dashboard.php">Dashboard</a> » FAQ Builder</div> <div class="container"> <h1>FAQ Builder for <?=esc($niceId)?><?=$paid?'<span class="badge">verified creator</span>':''?></h1> <div id="nav" class="btn-row" style="margin-bottom:1.4rem"> <select id="catSel"></select> <button class="alt" id="addCatBtn">+ New Category</button> <button class="danger" id="delCatBtn">Delete Cat</button> <button class="tool" onclick="downloadJSON()">📥 Download JSON</button> </div> <!-- QA editor --> <form id="qaForm" class="form-card"> <input type="hidden" name="cat" value="0"> <input type="hidden" name="idx" value="0"> <label>Question<input name="q" maxlength="200" required></label> <label>Answer<textarea name="a" required></textarea></label> <div class="btn-row"> <button class="primary" id="saveBtn">Save changes</button> <button class="alt" id="newBtn">Save as new</button> <button class="danger" type="button" id="delBtn">Delete Q/A</button> <button type="button" class="alt" id="upBtn">▲ Up</button> <button type="button" class="alt" id="dnBtn">▼ Down</button> </div> <div id="msg" class="notice"></div> </form> <!-- live preview --> <div class="preview-card"> <h3 id="pvCat" style="margin:0 0 .6rem;color:#df6200"></h3> <div id="pvBox"></div> </div> <!-- restore --> <div style="margin-top:2rem"> <label style="font-weight:600">Restore from JSON: <input type="file" id="upJson" accept=".json"></label> <div id="upMsg" class="notice"></div> </div> </div> <script> const idParam = <?=json_encode($mode==='biz'?'ph='.$phone:'user='.$slug)?>; let DATA=[]; // full faq array let curCat=0, curIdx=0; /* -------------- helpers ------------------ */ const $=q=>document.querySelector(q); function api(body){ return fetch('?'+idParam,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json()); } function refreshCatSel(){ const sel=$('#catSel'); sel.innerHTML=''; DATA.forEach((c,i)=>{const o=document.createElement('option');o.value=i;o.textContent=c.name||`Category ${i+1}`;sel.appendChild(o);}); sel.value=curCat; } function refreshPV(){ $('#pvCat').textContent=DATA[curCat]?.name||'FAQ'; const box=$('#pvBox'); box.innerHTML=''; (DATA[curCat]?.faqs||[]).forEach((qa,i)=>{ if(!qa.active) return; const dt=document.createElement('details'); if(i===curIdx) dt.open=true; const sum=document.createElement('summary'); sum.textContent=qa.q; dt.appendChild(sum); const p=document.createElement('div'); p.className='a'; p.innerHTML=qa.a.replace(/\n/g,'<br>'); /* ✎ Edit button (only visible when <details> is open via CSS) */ const pen=document.createElement('span'); pen.className='edit-btn'; pen.textContent='✎ Edit'; pen.title='Edit this Q/A'; pen.onclick=()=>{ curIdx=i; refreshForm(); window.scrollTo({top:$('#qaForm').offsetTop-15,behavior:'smooth'}); }; p.appendChild(pen); dt.appendChild(p); box.appendChild(dt); }); } /* -------------- load ------------------ */ async function load(){ const j=await fetch('?'+idParam+'&fetch=1').then(r=>r.json()); DATA=j.data.length?j.data:[{name:'General',faqs:[]}]; curCat=0; curIdx=0; refreshCatSel(); refreshForm(); refreshPV(); } function refreshForm(){ const qa=DATA[curCat]?.faqs[curIdx]||{q:'',a:''}; $('[name=q]').value=qa.q; $('[name=a]').value=qa.a; $('[name=cat]').value=curCat; $('[name=idx]').value=curIdx; $('#msg').textContent=''; } /* -------------- cat ops ---------------- */ $('#catSel').onchange=e=>{curCat=+e.target.value;curIdx=0;refreshForm();refreshPV();} $('#addCatBtn').onclick=async()=>{ const name=prompt('Category name?','New Category'); if(!name) return; const j=await api({action:'newCat',name}); curCat=j.cat; curIdx=0; DATA[curCat]={name,faqs:[]}; refreshCatSel(); refreshForm(); refreshPV(); }; $('#delCatBtn').onclick=async()=>{ if(!confirm('Delete this category (and all its Q/A)?')) return; await api({action:'deleteCat',cat:curCat}); DATA.splice(curCat,1); curCat=Math.max(0,curCat-1);curIdx=0; if(!DATA.length) DATA=[{name:'General',faqs:[]}]; refreshCatSel(); refreshForm(); refreshPV(); }; /* -------------- QA ops ----------------- */ function gather(){ return {cat:curCat,idx:curIdx,q:$('[name=q]').value.trim(),a:$('[name=a]').value.trim()}; } $('#saveBtn').onclick=async e=>{ e.preventDefault(); if(!gather().q||!gather().a) return; const j=await api({action:'saveQA',...gather()}); curIdx=j.idx; DATA[curCat].faqs[curIdx]={q:gather().q,a:gather().a,active:true}; refreshCatSel(); refreshForm(); refreshPV(); $('#msg').textContent='Saved ✔'; }; $('#newBtn').onclick=async e=>{ e.preventDefault(); if(!gather().q||!gather().a) return; const j=await api({action:'saveQA',cat:curCat,idx:999999,q:gather().q,a:gather().a}); curIdx=j.idx; DATA[curCat].faqs.push({q:gather().q,a:gather().a,active:true}); refreshCatSel(); refreshForm(); refreshPV(); $('#msg').textContent='Added ✔'; }; $('#delBtn').onclick=async()=>{ if(!DATA[curCat].faqs.length) return; if(!confirm('Delete this Q/A?')) return; await api({action:'deleteQA',cat:curCat,idx:curIdx}); DATA[curCat].faqs.splice(curIdx,1); curIdx=Math.max(0,curIdx-1);refreshCatSel();refreshForm();refreshPV(); }; $('#upBtn').onclick=()=>move(-1); $('#dnBtn').onclick=()=>move(+1); async function move(delta){ const to=curIdx+delta; if(to<0||to>=DATA[curCat].faqs.length) return; await api({action:'moveQA',cat:curCat,idx:curIdx,to}); const faqs=DATA[curCat].faqs; [faqs[curIdx],faqs[to]]=[faqs[to],faqs[curIdx]]; curIdx=to; refreshCatSel(); refreshPV(); refreshForm(); } /* -------------- upload / download ------ */ function downloadJSON(){ location.href='?'+idParam+'&download=1'; } $('#upJson').onchange=async e=>{ const f=e.target.files[0]; if(!f) return; try{ const raw=await f.text(); JSON.parse(raw); const ok=(await api({action:'upload',blob:raw})).success; $('#upMsg').textContent=ok?'Imported ✔':'Failed'; await load(); }catch{ $('#upMsg').textContent='Invalid JSON'; } }; /* -------------- init ------------------ */ load(); </script> </body></html>
Save changes
Create folder
writable 0777
Create
Cancel