(function(){ 'use strict'; const API='https://epictravelexpeditions.com/api'; function authHdr(){return{'Content-Type':'application/json',Authorization:`Bearer ${localStorage.getItem('auth_token')}`};} async function api(path,opts){ const res=await fetch(API+path,{headers:authHdr(),...opts}); const d=await res.json(); if(!res.ok)throw new Error(d.error||res.statusText); return d; } async function uploadImg(file){ const fd=new FormData();fd.append('file',file); const res=await fetch(API+'/upload/image',{method:'POST',headers:{Authorization:`Bearer ${localStorage.getItem('auth_token')}`},body:fd}); const d=await res.json(); if(!res.ok)throw new Error(d.error||'Upload failed'); return d.url; } async function getImg(inputId,fallback){ const el=document.getElementById(inputId); if(el&&el.files&&el.files[0]){ const st=document.getElementById(inputId+'-st'); if(st)st.textContent='Uploading…'; try{const url=await uploadImg(el.files[0]);if(st)st.textContent='';return url;} catch(e){if(st){st.textContent='Upload failed: '+e.message;st.style.color='#dc2626';}throw e;} } return fallback!=null?fallback:null; } function imgPicker(id,src){ const pr=src ?`` :`
πŸ–Ό
`; return `
${pr}
`; } function daysAgo(s){return Math.floor((Date.now()-new Date(s))/86400000);} function daysLeft(s){return Math.ceil((new Date(s)-Date.now())/86400000);} function ageBdg(d){const c=d<30?'bg':d<90?'by':'br';return`${d}d ago`;} function expBdg(d){if(d<0)return`Expired`;const c=d>14?'bg':d>7?'by':'br';return`${d}d left`;} function esc(s){const e=document.createElement('div');e.textContent=s||'';return e.innerHTML;} function catOpts(cats,sel){return cats.map(c=>``).join('');} function fmtPrice(p){return p!=null?'$'+parseFloat(p).toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:2}):'';} /* CSS */ const css=` #ep-portal{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#111827;min-height:100vh;background:#f3f4f6} #ep-portal *,#ep-portal *::before,#ep-portal *::after{box-sizing:border-box} #ep-hdr{background:#fff;border-bottom:1px solid #e5e7eb;padding:0 28px;height:60px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;box-shadow:0 1px 3px rgba(0,0,0,.06)} .ep-logo{font-size:17px;font-weight:800;color:#1d4ed8;letter-spacing:-.4px}.ep-logo span{color:#6b7280;font-weight:400} .ep-hdr-acts{display:flex;gap:8px} .ep-hbtn{padding:6px 14px;border-radius:7px;font-size:13px;font-weight:600;border:1px solid #d1d5db;background:#fff;color:#374151;cursor:pointer;transition:all .15s;text-decoration:none;display:inline-flex;align-items:center;gap:5px} .ep-hbtn:hover{background:#f9fafb}.ep-hbtn.red{border-color:#fca5a5;color:#dc2626}.ep-hbtn.red:hover{background:#fef2f2} #ep-tabs{background:#fff;border-bottom:1px solid #e5e7eb;padding:0 28px;display:flex;gap:2px} .ep-tab{padding:13px 18px;font-size:13px;font-weight:600;color:#6b7280;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .15s;display:inline-flex;align-items:center;gap:7px} .ep-tab:hover{color:#1d4ed8}.ep-tab.active{color:#1d4ed8;border-bottom-color:#1d4ed8} .ep-tbadge{background:#fee2e2;color:#dc2626;border-radius:999px;font-size:11px;padding:1px 6px;font-weight:700} #ep-body{padding:28px;max-width:1180px;margin:0 auto} .ep-g4{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:28px} @media(max-width:880px){.ep-g4{grid-template-columns:repeat(2,1fr)}} .ep-stat{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 22px;position:relative;overflow:hidden} .ep-si{font-size:26px;margin-bottom:6px}.ep-sv{font-size:30px;font-weight:800;color:#111827}.ep-sl{font-size:12px;color:#6b7280;margin-top:1px} .ep-acc{position:absolute;right:0;top:0;bottom:0;width:4px;border-radius:0 12px 12px 0} .acb{background:#3b82f6}.acg{background:#10b981}.aca{background:#f59e0b}.acp{background:#8b5cf6} .ep-card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;margin-bottom:22px;overflow:hidden} .ep-ch{padding:16px 22px;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px} .ep-ct{font-size:14px;font-weight:700;color:#111827}.ep-cs{font-size:12px;color:#6b7280;margin-top:1px} .ep-cb{padding:22px} .ep-tbl{width:100%;border-collapse:collapse;font-size:13px} .ep-tbl th{text-align:left;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.05em;padding:9px 12px;border-bottom:1px solid #e5e7eb;white-space:nowrap} .ep-tbl td{padding:11px 12px;border-bottom:1px solid #f3f4f6;vertical-align:middle} .ep-tbl tr:last-child td{border-bottom:none}.ep-tbl tr:hover td{background:#fafafa} .ep-b{display:inline-block;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:700} .bg{background:#d1fae5;color:#065f46}.by{background:#fef3c7;color:#92400e}.br{background:#fee2e2;color:#991b1b}.bb{background:#dbeafe;color:#1e40af}.bgr{background:#f3f4f6;color:#374151} .ep-btn{padding:6px 13px;border-radius:7px;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:opacity .15s;display:inline-flex;align-items:center;gap:4px;white-space:nowrap} .ep-btn:hover{opacity:.82}.ep-btn:disabled{opacity:.4;cursor:not-allowed} .ep-pri{background:#2563eb;color:#fff}.ep-suc{background:#16a34a;color:#fff}.ep-dan{background:#dc2626;color:#fff}.ep-gho{background:#f3f4f6;color:#374151} .ep-form{display:grid;grid-template-columns:1fr 1fr;gap:14px} @media(max-width:640px){.ep-form{grid-template-columns:1fr}} .ep-full{grid-column:1/-1}.ep-fld{display:flex;flex-direction:column;gap:4px} .ep-lbl{font-size:11px;font-weight:700;color:#374151;text-transform:uppercase;letter-spacing:.04em} .ep-inp,.ep-sel,.ep-ta{padding:8px 11px;border:1px solid #d1d5db;border-radius:7px;font-size:13px;color:#111827;background:#fff;outline:none;transition:border-color .15s;width:100%} .ep-inp:focus,.ep-sel:focus,.ep-ta:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.1)} .ep-ta{resize:vertical;min-height:72px} .ep-frow{grid-column:1/-1;display:flex;gap:10px;justify-content:flex-end;padding-top:4px;align-items:center} .ep-addbox{border:1px dashed #d1d5db;border-radius:10px;padding:18px;margin-bottom:20px;background:#fafafa;display:none} .ep-addbox.open{display:block} .ep-thumb{width:40px;height:40px;border-radius:7px;object-fit:cover;background:#e5e7eb} .ep-smsg{font-size:12px}.ep-smsg.ok{color:#16a34a}.ep-smsg.err{color:#dc2626} .ep-alert{padding:10px 15px;border-radius:8px;font-size:13px;margin-bottom:16px;border-left:3px solid} .ep-alert.warn{background:#fffbeb;color:#92400e;border-color:#f59e0b} .ep-inline{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:14px} @media(max-width:640px){.ep-inline{grid-column:1fr}} .ep-cat-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;margin-bottom:8px} .ep-cat-nm{flex:1;font-size:13px;font-weight:600}.ep-cat-ct{font-size:11px;color:#9ca3af} .ep-cat-ei{flex:1;padding:5px 9px;border:1px solid #3b82f6;border-radius:6px;font-size:13px;outline:none} .ep-tc{border:1px solid #e5e7eb;border-radius:10px;padding:16px;margin-bottom:12px;display:flex;gap:12px;background:#fff} .ep-tav{width:42px;height:42px;border-radius:50%;object-fit:cover;flex-shrink:0;background:#e5e7eb} .ep-tavph{width:42px;height:42px;border-radius:50%;background:#2563eb;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;flex-shrink:0} .ep-tb{flex:1;min-width:0}.ep-tn{font-weight:700;font-size:13px}.ep-tl{font-size:11px;color:#6b7280;margin-bottom:5px} .ep-tm{font-size:13px;color:#374151;margin-bottom:8px;line-height:1.5} .ep-te{width:100%;border:1px solid #d1d5db;border-radius:6px;padding:6px 9px;font-size:12px;margin-bottom:7px;resize:vertical;min-height:52px} .ep-tacts{display:flex;gap:7px;flex-wrap:wrap} .ep-ftabs{display:flex;gap:7px;margin-bottom:18px;flex-wrap:wrap} .ep-ft{padding:6px 15px;border-radius:7px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid #d1d5db;background:#fff;color:#374151;transition:all .15s} .ep-ft.active{background:#2563eb;color:#fff;border-color:#2563eb} .ep-empty{text-align:center;padding:36px;color:#9ca3af;font-size:13px} .ep-spec-img{width:52px;height:52px;border-radius:8px;object-fit:cover;background:#e5e7eb;flex-shrink:0} .ep-price-old{text-decoration:line-through;color:#9ca3af;font-size:11px} .ep-price-new{color:#16a34a;font-weight:700} `; const sel=document.createElement('style');sel.textContent=css;document.head.appendChild(sel); /* State */ let activeTab='dashboard',tFilter='pending'; let destinations=[],specials=[],testimonials=[],categories=[]; async function loadAll(){ const r=await Promise.allSettled([ fetch(API+'/destinations',{headers:authHdr()}).then(r=>r.json()), fetch(API+'/specials', {headers:authHdr()}).then(r=>r.json()), fetch(API+'/testimonials/all',{headers:authHdr()}).then(r=>r.json()), fetch(API+'/categories', {headers:authHdr()}).then(r=>r.json()) ]); if(r[0].status==='fulfilled'&&Array.isArray(r[0].value))destinations=r[0].value; if(r[1].status==='fulfilled'&&Array.isArray(r[1].value))specials=r[1].value; if(r[2].status==='fulfilled'&&Array.isArray(r[2].value))testimonials=r[2].value; if(r[3].status==='fulfilled'&&Array.isArray(r[3].value))categories=r[3].value; } /* Shell */ function buildShell(){ const p=document.createElement('div');p.id='ep-portal'; p.innerHTML=`
🌐 View Site
`; p.querySelector('#ep-logout').onclick=()=>{localStorage.removeItem('isAdminAuthenticated');localStorage.removeItem('auth_token');window.location.href='/admin';}; p.querySelectorAll('.ep-tab[data-tab]').forEach(b=>{b.onclick=()=>{activeTab=b.dataset.tab;p.querySelectorAll('.ep-tab').forEach(t=>t.classList.remove('active'));b.classList.add('active');render();};}); return p; } /* Dashboard */ function rDash(){ const dm=Object.fromEntries(destinations.map(d=>[d.id,d])); const pen=testimonials.filter(t=>t.status==='pending').length; const app=testimonials.filter(t=>t.status==='approved').length; let h=`
πŸ—Ί
${destinations.length}
Destinations
⭐
${specials.length}
Active Specials
⏳
${pen}
Pending Reviews
βœ…
${app}
Approved Testimonials
`; if(pen>0)h+=`
⚠️ ${pen} testimonial${pen>1?'s':''} awaiting review.
`; /* Destinations age */ const sd=[...destinations].sort((a,b)=>new Date(a.created_at)-new Date(b.created_at)); h+=`
πŸ—Ί Destinations
Oldest entries first
`; sd.forEach(d=>{h+=``;}); h+=`
DestinationCategoryPriceIn System Since
${esc(d.name)} ${esc(d.location)}${esc(d.category)}${fmtPrice(d.price)}${ageBdg(daysAgo(d.created_at))}
`; /* Specials expiry */ const ss=[...specials].sort((a,b)=>new Date(a.end_date)-new Date(b.end_date)); h+=`
⭐ Weekly Specials
Expiring soonest first
`; ss.forEach(s=>{ const dest=dm[s.destination_id]; const basePrice=s.price!=null?parseFloat(s.price):(dest?parseFloat(dest.price):0); const salePrice=basePrice*(1-parseFloat(s.discount)/100); const imgSrc=s.image_path||(dest?dest.image:''); h+=``; }); h+=`
ImageDestinationOriginal PriceDiscountSale PriceExpiresStatus
${imgSrc?``:''} ${esc(dest?dest.name:s.destination_id)} ${fmtPrice(basePrice)} ${parseFloat(s.discount)}% OFF ${fmtPrice(salePrice)} ${new Date(s.end_date).toLocaleDateString()} ${expBdg(daysLeft(s.end_date))}
`; /* Pending testimonials */ const pl=testimonials.filter(t=>t.status==='pending'); if(pl.length){ h+=`
⏳ Pending Testimonials
`; pl.forEach(t=>{ const av=t.image_path?``:`
${esc(t.full_name.charAt(0))}
`; h+=`
${av}
${esc(t.full_name)}
${esc(t.location)}
${esc(t.message)}
`; }); h+=`
`; } return h; } /* Destinations */ function destRow(d){ return` ${esc(d.name)}
${esc(d.location)} ${esc(d.category)} ${fmtPrice(d.price)} ⭐ ${parseFloat(d.rating).toFixed(1)} ${ageBdg(daysAgo(d.created_at))} `; } function rDest(){ const cc={};destinations.forEach(d=>{cc[d.category]=(cc[d.category]||0)+1;}); return`
🏷 Categories
Add, rename, or remove destination categories
${categories.map(c=>{const n=cc[c.name]||0;return`
${esc(c.name)}${n} destination${n!==1?'s':''}
`;}).join('')}
πŸ—Ί All Destinations (${destinations.length})
New Destination
Name *
Location *
Category *
Price (USD) *
Rating (1–5)
Image *${imgPicker('ep-dn-img','')}
Description *
${destinations.map(destRow).join('')}
PhotoName & LocationCategoryPriceRatingIn SystemActions
`; } /* Specials */ function specImg(s){ const dm=Object.fromEntries(destinations.map(d=>[d.id,d])); const dest=dm[s.destination_id]; return s.image_path||(dest?dest.image:'')||''; } function specRow(s){ const dm=Object.fromEntries(destinations.map(d=>[d.id,d])); const dest=dm[s.destination_id]; const base=s.price!=null?parseFloat(s.price):(dest?parseFloat(dest.price):0); const sale=base*(1-parseFloat(s.discount)/100); const img=specImg(s); return` ${img?``:''} ${esc(dest?dest.name:s.destination_id)} ${parseFloat(s.discount)}% OFF ${fmtPrice(base)} ${fmtPrice(sale)} ${new Date(s.end_date).toLocaleDateString()} ${expBdg(daysLeft(s.end_date))} `; } function rSpec(){ const destOpts=destinations.map(d=>``).join(''); return`
⭐ Weekly Specials (${specials.length})
New Special
Destination *
Original Price (USD) *
Discount % *
End Date *
Special Image (optional β€” defaults to destination photo)${imgPicker('ep-sn-img','')}
Highlights (one per line)
${specials.map(specRow).join('')}
ImageDestinationDiscountOriginal PriceSale PriceEnd DateStatusActions
`; } /* Testimonials */ function rTest(){ const cn={pending:0,approved:0,denied:0};testimonials.forEach(t=>cn[t.status]++); const list=tFilter==='all'?testimonials:testimonials.filter(t=>t.status===tFilter); let h=`
`; if(!list.length)return h+`
πŸ’¬ No testimonials here.
`; list.forEach(t=>{ const av=t.image_path?``:`
${esc(t.full_name.charAt(0))}
`; const sb=`${t.status}`; h+=`
${av}
${esc(t.full_name)} ${sb}
${esc(t.location)}
${esc(t.message)}
${t.status!=='approved'?``:''} ${t.status!=='denied'?``:''}
`; }); return h; } /* Render */ function render(){ const body=document.getElementById('ep-body');if(!body)return; switch(activeTab){ case'dashboard': body.innerHTML=rDash(); wireDash(); break; case'destinations':body.innerHTML=rDest(); wireDest(); break; case'specials': body.innerHTML=rSpec(); wireSpec(); break; case'testimonials':body.innerHTML=rTest(); wireTest(); break; } const pb=document.getElementById('ep-pb'); if(pb){const n=testimonials.filter(t=>t.status==='pending').length;pb.textContent=n||'';pb.style.display=n?'':'none';} } /* Wire: Dashboard */ function wireDash(){ document.querySelectorAll('[data-qa]').forEach(b=>b.onclick=async()=>{await api(`/testimonials/${b.dataset.qa}`,{method:'PUT',body:JSON.stringify({status:'approved'})});await loadAll();render();}); document.querySelectorAll('[data-qd]').forEach(b=>b.onclick=async()=>{await api(`/testimonials/${b.dataset.qd}`,{method:'PUT',body:JSON.stringify({status:'denied'})});await loadAll();render();}); } /* Wire: Destinations */ function wireDest(){ /* Categories */ const ct=document.getElementById('ep-cat-tog'),cb=document.getElementById('ep-cat-box'); if(ct)ct.onclick=()=>cb.classList.toggle('open'); const cc=document.getElementById('ep-cat-cancel');if(cc)cc.onclick=()=>cb.classList.remove('open'); const cs=document.getElementById('ep-cat-save'); if(cs)cs.onclick=async()=>{ const m=document.getElementById('ep-cat-msg'),nm=document.getElementById('ep-cat-new').value.trim(); if(!nm){m.textContent='Name required';m.className='ep-smsg err';return;} try{await api('/categories',{method:'POST',body:JSON.stringify({name:nm})});m.textContent='Added!';m.className='ep-smsg ok';await loadAll();activeTab='destinations';render();} catch(e){m.textContent=e.message;m.className='ep-smsg err';} }; document.querySelectorAll('[data-ren]').forEach(b=>b.onclick=()=>{ const id=b.dataset.ren,cur=b.dataset.cv,row=b.closest('.ep-cat-row'); row.innerHTML=``; row.querySelector('[data-rc]').onclick=()=>{activeTab='destinations';render();}; row.querySelector(`[data-rs="${id}"]`).onclick=async()=>{ const nm=document.getElementById(`ep-ren-${id}`).value.trim(),m=document.getElementById(`ep-rm-${id}`); if(!nm){m.textContent='Required';m.className='ep-smsg err';return;} try{await api(`/categories/${id}`,{method:'PUT',body:JSON.stringify({name:nm})});await loadAll();activeTab='destinations';render();} catch(e){m.textContent=e.message;m.className='ep-smsg err';} }; }); document.querySelectorAll('[data-dc]').forEach(b=>b.onclick=async()=>{ if(!confirm(`Delete category "${b.dataset.dn}"?`))return; try{await api(`/categories/${b.dataset.dc}`,{method:'DELETE'});await loadAll();activeTab='destinations';render();}catch(e){alert(e.message);} }); /* Destination add */ const dt=document.getElementById('ep-dest-tog'),db=document.getElementById('ep-dest-box'); if(dt)dt.onclick=()=>db.classList.toggle('open'); const dn=document.getElementById('ep-dn-cancel');if(dn)dn.onclick=()=>db.classList.remove('open'); const ds=document.getElementById('ep-dn-save'); if(ds)ds.onclick=async()=>{ const m=document.getElementById('ep-dn-msg'); try{ const img=await getImg('ep-dn-img',''); await api('/destinations',{method:'POST',body:JSON.stringify({ name:document.getElementById('ep-dn-name').value.trim(), location:document.getElementById('ep-dn-loc').value.trim(), category:document.getElementById('ep-dn-cat').value, price:document.getElementById('ep-dn-price').value, rating:document.getElementById('ep-dn-rating').value||'4.5', image:img, description:document.getElementById('ep-dn-desc').value.trim() })}); m.textContent='Saved!';m.className='ep-smsg ok';await loadAll();activeTab='destinations';render(); }catch(e){m.textContent=e.message;m.className='ep-smsg err';} }; /* Destination edit */ document.querySelectorAll('[data-ed]').forEach(b=>b.onclick=()=>{ const id=b.dataset.ed,d=destinations.find(x=>x.id==id);if(!d)return; const row=document.querySelector(`tr[data-did="${id}"]`); row.innerHTML=`
Name
Location
Category
Price
Rating
Image (upload new to replace)${imgPicker('ei-i-'+id,d.image)}
Description
`; row.querySelector(`[data-ec="${id}"]`).onclick=()=>{row.outerHTML=destRow(d);wireDest();}; row.querySelector(`[data-es="${id}"]`).onclick=async()=>{ try{ const img=await getImg('ei-i-'+id,d.image); await api(`/destinations/${id}`,{method:'PUT',body:JSON.stringify({ name:document.getElementById(`ei-n-${id}`).value, location:document.getElementById(`ei-l-${id}`).value, category:document.getElementById(`ei-c-${id}`).value, price:document.getElementById(`ei-p-${id}`).value, rating:document.getElementById(`ei-r-${id}`).value, image:img, description:document.getElementById(`ei-d-${id}`).value })}); await loadAll();activeTab='destinations';render(); }catch(e){alert(e.message);} }; }); /* Destination delete */ document.querySelectorAll('[data-dd]').forEach(b=>b.onclick=async()=>{ if(!confirm('Delete this destination? This cannot be undone.'))return; try{await api(`/destinations/${b.dataset.dd}`,{method:'DELETE'});await loadAll();activeTab='destinations';render();}catch(e){alert(e.message);} }); } /* Wire: Specials */ function wireSpec(){ const st=document.getElementById('ep-spec-tog'),sb=document.getElementById('ep-spec-box'); if(st)st.onclick=()=>sb.classList.toggle('open'); const sc=document.getElementById('ep-sn-cancel');if(sc)sc.onclick=()=>sb.classList.remove('open'); const ss=document.getElementById('ep-sn-save'); if(ss)ss.onclick=async()=>{ const m=document.getElementById('ep-sn-msg'); try{ const img=await getImg('ep-sn-img',null); const hls=document.getElementById('ep-sn-hls').value.split('\n').map(l=>l.trim()).filter(Boolean); await api('/specials',{method:'POST',body:JSON.stringify({ destination_id:document.getElementById('ep-sn-dest').value, price:document.getElementById('ep-sn-price').value, discount:document.getElementById('ep-sn-disc').value, end_date:document.getElementById('ep-sn-end').value, image_path:img, highlights:hls })}); m.textContent='Saved!';m.className='ep-smsg ok';await loadAll();activeTab='specials';render(); }catch(e){m.textContent=e.message;m.className='ep-smsg err';} }; /* Specials edit */ document.querySelectorAll('[data-es]').forEach(b=>b.onclick=()=>{ const id=b.dataset.es,s=specials.find(x=>x.id==id);if(!s)return; const dm=Object.fromEntries(destinations.map(d=>[d.id,d])); const dest=dm[s.destination_id]; const hls=Array.isArray(s.highlights)?s.highlights.join('\n'):''; const curImg=specImg(s); const row=document.querySelector(`tr[data-sid="${id}"]`); row.innerHTML=`
Original Price
Discount %
End Date
Image (upload to replace; leave blank to use destination photo)${imgPicker('si-i-'+id,curImg)}
Highlights (one per line)
`; row.querySelector(`[data-sc="${id}"]`).onclick=()=>{row.outerHTML=specRow(s);wireSpec();}; row.querySelector(`[data-sv="${id}"]`).onclick=async()=>{ try{ const newImg=await getImg('si-i-'+id,s.image_path||null); const hls2=document.getElementById(`si-h-${id}`).value.split('\n').map(l=>l.trim()).filter(Boolean); await api(`/specials/${id}`,{method:'PUT',body:JSON.stringify({ price:document.getElementById(`si-p-${id}`).value, discount:document.getElementById(`si-d-${id}`).value, end_date:document.getElementById(`si-e-${id}`).value, image_path:newImg, highlights:hls2 })}); await loadAll();activeTab='specials';render(); }catch(e){alert(e.message);} }; }); /* Specials delete */ document.querySelectorAll('[data-ds]').forEach(b=>b.onclick=async()=>{ if(!confirm('Delete this special?'))return; try{await api(`/specials/${b.dataset.ds}`,{method:'DELETE'});await loadAll();activeTab='specials';render();}catch(e){alert(e.message);} }); } /* Wire: Testimonials */ function wireTest(){ document.querySelectorAll('[data-tf]').forEach(b=>b.onclick=()=>{tFilter=b.dataset.tf;render();}); document.querySelectorAll('[data-ta]').forEach(b=>b.onclick=async()=>{ const id=b.dataset.tid,action=b.dataset.ta; const card=document.querySelector(`.ep-tc[data-tid="${id}"]`); try{ if(action==='approve')await api(`/testimonials/${id}`,{method:'PUT',body:JSON.stringify({status:'approved'})}); else if(action==='deny')await api(`/testimonials/${id}`,{method:'PUT',body:JSON.stringify({status:'denied'})}); else if(action==='save'){const ta=card.querySelector('.ep-te');await api(`/testimonials/${id}`,{method:'PUT',body:JSON.stringify({message:ta.value})});} else if(action==='delete'){if(!confirm('Delete?'))return;await api(`/testimonials/${id}`,{method:'DELETE'});} await loadAll();render(); }catch(e){alert(e.message);} }); } /* Init */ let injected=false; function tryInject(){ if(injected)return; if(!window.location.pathname.startsWith('/admin/dashboard'))return; if(!localStorage.getItem('isAdminAuthenticated'))return; const root=document.getElementById('root'); if(!root||!root.children.length)return; injected=true;obs.disconnect(); Array.from(root.children).forEach(c=>c.style.display='none'); const portal=buildShell();root.insertBefore(portal,root.firstChild); loadAll().then(render); } const obs=new MutationObserver(tryInject); obs.observe(document.body,{childList:true,subtree:true}); tryInject(); })();