2
0
Files
gitcaddy-server/templates/pages/base_footer.tmpl
logikonline 818c2db411
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m23s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Lint (push) Successful in 6m8s
Build and Release / Unit Tests (push) Successful in 6m12s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m37s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h5m0s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Failing after 1s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Failing after 0s
Build and Release / Build Binary (linux/arm64) (push) Failing after 13m52s
feat(pages): add blog reading enhancements and gallery lightbox
Add reading progress bar, table of contents sidebar, and back-to-top button for blog post detail pages. TOC auto-generates from h1-h4 headings with smooth scroll navigation and active section highlighting. Sidebar expands on hover from left edge. Add lightbox for gallery images with keyboard navigation (arrows, escape) and captions. Back-to-top button appears after 400px scroll on all pages. All features use inline styles for portability and minimal CSS conflicts.
2026-03-16 21:43:46 -04:00

224 lines
10 KiB
Handlebars

{{if .GalleryImages}}
<div id="pages-lightbox" style="display:none; position:fixed; inset:0; z-index:9999; background:rgba(0,0,0,0.92); align-items:center; justify-content:center; flex-direction:column;">
<button id="pages-lb-close" style="position:absolute; top:16px; right:20px; background:none; border:none; color:#fff; font-size:32px; cursor:pointer; z-index:2; line-height:1;" aria-label="Close">&times;</button>
<button id="pages-lb-prev" style="position:absolute; left:16px; top:50%; transform:translateY(-50%); background:none; border:none; color:#fff; font-size:28px; cursor:pointer; z-index:2;" aria-label="Previous">&#8249;</button>
<button id="pages-lb-next" style="position:absolute; right:16px; top:50%; transform:translateY(-50%); background:none; border:none; color:#fff; font-size:28px; cursor:pointer; z-index:2;" aria-label="Next">&#8250;</button>
<img id="pages-lb-img" src="" alt="" style="max-width:90vw; max-height:82vh; object-fit:contain; border-radius:4px; user-select:none;">
<div id="pages-lb-caption" style="color:#ccc; font-size:14px; margin-top:12px; text-align:center; max-width:80vw;"></div>
</div>
<script>
(function(){
var lb=document.getElementById('pages-lightbox'),img=document.getElementById('pages-lb-img'),
cap=document.getElementById('pages-lb-caption'),items=[],idx=0;
document.querySelectorAll('.pages-gallery-trigger').forEach(function(el,i){
items.push({src:el.dataset.src,caption:el.dataset.caption||''});
el.addEventListener('click',function(e){e.preventDefault();idx=i;show();});
});
function show(){
if(!items.length)return;
img.src=items[idx].src;img.alt=items[idx].caption;
cap.textContent=items[idx].caption;
document.getElementById('pages-lb-prev').style.display=idx>0?'':'none';
document.getElementById('pages-lb-next').style.display=idx<items.length-1?'':'none';
lb.style.display='flex';document.body.style.overflow='hidden';
}
function hide(){lb.style.display='none';document.body.style.overflow='';}
document.getElementById('pages-lb-close').addEventListener('click',hide);
document.getElementById('pages-lb-prev').addEventListener('click',function(){if(idx>0){idx--;show();}});
document.getElementById('pages-lb-next').addEventListener('click',function(){if(idx<items.length-1){idx++;show();}});
lb.addEventListener('click',function(e){if(e.target===lb)hide();});
document.addEventListener('keydown',function(e){
if(lb.style.display!=='flex')return;
if(e.key==='Escape')hide();
else if(e.key==='ArrowLeft'&&idx>0){idx--;show();}
else if(e.key==='ArrowRight'&&idx<items.length-1){idx++;show();}
});
})();
</script>
{{end}}
{{if .PageIsBlogDetail}}
<!-- Reading Progress Bar -->
<div id="blog-progress" style="position:fixed; top:0; left:0; width:100%; height:3px; z-index:9998; background:transparent; pointer-events:none;">
<div id="blog-progress-bar" style="height:100%; width:0; background:var(--pages-primary,#4183c4); transition:width 80ms linear;"></div>
</div>
<!-- Table of Contents Sidebar -->
<div id="blog-toc" style="position:fixed; left:0; top:50%; transform:translateY(-50%); z-index:9997; display:none;">
<div id="blog-toc-tab" style="width:36px; height:36px; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.65); backdrop-filter:blur(8px); border-radius:0 8px 8px 0; cursor:pointer; color:#fff; font-size:14px; transition:opacity 0.2s;" title="Table of Contents">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="3" y1="4" x2="13" y2="4"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="3" y1="12" x2="10" y2="12"/></svg>
</div>
<div id="blog-toc-panel" style="position:absolute; left:0; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.78); backdrop-filter:blur(12px); border-radius:0 10px 10px 0; padding:16px 20px 16px 16px; min-width:220px; max-width:300px; max-height:70vh; overflow-y:auto; opacity:0; pointer-events:none; transition:opacity 0.2s, transform 0.2s; transform:translateY(-50%) translateX(-8px);">
<div style="font-size:11px; text-transform:uppercase; letter-spacing:0.08em; color:rgba(255,255,255,0.5); margin-bottom:10px; font-weight:600;">Contents</div>
<ul id="blog-toc-list" style="list-style:none; margin:0; padding:0;"></ul>
</div>
</div>
<!-- Back to Top Button -->
<button id="blog-back-top" style="position:fixed; bottom:32px; right:32px; z-index:9997; width:44px; height:44px; border-radius:50%; border:none; background:rgba(0,0,0,0.65); backdrop-filter:blur(8px); color:#fff; font-size:20px; cursor:pointer; display:flex; align-items:center; justify-content:center; opacity:0; pointer-events:none; transition:opacity 0.3s, transform 0.3s; transform:translateY(12px); box-shadow:0 2px 12px rgba(0,0,0,0.25);" aria-label="Back to top">
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V3"/><path d="M3 7l5-5 5 5"/></svg>
</button>
<script>
(function(){
/* --- Heading scan & TOC build --- */
var content=document.querySelector('.markup');
if(!content)return;
var headings=content.querySelectorAll('h1,h2,h3,h4');
var tocList=document.getElementById('blog-toc-list');
var tocWrap=document.getElementById('blog-toc');
var tocTab=document.getElementById('blog-toc-tab');
var tocPanel=document.getElementById('blog-toc-panel');
var slugs={};
if(headings.length>=2&&tocList){
tocWrap.style.display='block';
headings.forEach(function(h){
/* generate slug ID */
var text=h.textContent.trim();
var slug=text.toLowerCase().replace(/[^\w\s-]/g,'').replace(/\s+/g,'-').replace(/-+/g,'-');
if(!slug)slug='section';
if(slugs[slug]){slugs[slug]++;slug=slug+'-'+slugs[slug];}
else{slugs[slug]=1;}
h.id=slug;
var level=parseInt(h.tagName.charAt(1));
var indent=(level-1)*14;
var li=document.createElement('li');
li.style.marginBottom='6px';
var a=document.createElement('a');
a.href='#'+slug;
a.textContent=text;
a.style.cssText='color:rgba(255,255,255,0.75);text-decoration:none;font-size:13px;display:block;padding:3px 0 3px '+indent+'px;border-radius:4px;transition:color 0.15s,background 0.15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;';
a.addEventListener('mouseenter',function(){a.style.color='#fff';});
a.addEventListener('mouseleave',function(){if(!a.classList.contains('active'))a.style.color='rgba(255,255,255,0.75)';});
a.addEventListener('click',function(e){
e.preventDefault();
h.scrollIntoView({behavior:'smooth',block:'start'});
});
a.dataset.target=slug;
li.appendChild(a);
tocList.appendChild(li);
});
}
/* --- TOC hover expand --- */
var showTimer,hideTimer;
function showPanel(){
clearTimeout(hideTimer);
tocPanel.style.opacity='1';
tocPanel.style.pointerEvents='auto';
tocPanel.style.transform='translateY(-50%) translateX(0)';
tocTab.style.opacity='0';
}
function hidePanel(){
hideTimer=setTimeout(function(){
tocPanel.style.opacity='0';
tocPanel.style.pointerEvents='none';
tocPanel.style.transform='translateY(-50%) translateX(-8px)';
tocTab.style.opacity='1';
},200);
}
if(tocTab&&tocPanel){
tocTab.addEventListener('mouseenter',showPanel);
tocTab.addEventListener('mouseleave',hidePanel);
tocPanel.addEventListener('mouseenter',function(){clearTimeout(hideTimer);});
tocPanel.addEventListener('mouseleave',hidePanel);
}
/* --- Active heading tracking --- */
var tocLinks=tocList?tocList.querySelectorAll('a'):[];
function updateActive(){
var scrollTop=window.scrollY||document.documentElement.scrollTop;
var current=null;
headings.forEach(function(h){
if(h.getBoundingClientRect().top<=100)current=h.id;
});
tocLinks.forEach(function(a){
if(a.dataset.target===current){
a.style.color='#fff';a.style.background='rgba(255,255,255,0.1)';
a.classList.add('active');
}else{
a.style.color='rgba(255,255,255,0.75)';a.style.background='transparent';
a.classList.remove('active');
}
});
}
/* --- Progress bar --- */
var bar=document.getElementById('blog-progress-bar');
function updateProgress(){
var docH=document.documentElement.scrollHeight-window.innerHeight;
if(docH<=0){bar.style.width='100%';return;}
var pct=Math.min(100,Math.max(0,(window.scrollY/docH)*100));
bar.style.width=pct+'%';
}
/* --- Back to top --- */
var btt=document.getElementById('blog-back-top');
function updateBtt(){
if(window.scrollY>400){
btt.style.opacity='1';btt.style.pointerEvents='auto';btt.style.transform='translateY(0)';
}else{
btt.style.opacity='0';btt.style.pointerEvents='none';btt.style.transform='translateY(12px)';
}
}
if(btt){
btt.addEventListener('click',function(){window.scrollTo({top:0,behavior:'smooth'});});
}
/* --- Unified scroll handler --- */
var ticking=false;
window.addEventListener('scroll',function(){
if(!ticking){
requestAnimationFrame(function(){
updateProgress();
updateBtt();
if(headings.length>=2)updateActive();
ticking=false;
});
ticking=true;
}
});
updateProgress();
updateBtt();
if(headings.length>=2)updateActive();
})();
</script>
{{end}}
{{if .ABTestActive}}
<script>
(function() {
var vid = '{{.VisitorID}}';
var eid = {{.ExperimentID}};
var varid = {{.VariantID}};
var base = '{{.EventTrackURL}}';
function send(type, data) {
try {
navigator.sendBeacon(base, JSON.stringify({
event_type: type, visitor_id: vid,
experiment_id: eid, variant_id: varid, data: data || ''
}));
} catch(e) {}
}
send('impression');
document.querySelectorAll('[data-cta]').forEach(function(el) {
el.addEventListener('click', function() { send('cta_click', el.dataset.cta); });
});
var maxScroll = 0;
window.addEventListener('scroll', function() {
var pct = Math.round((window.scrollY + window.innerHeight) / document.body.scrollHeight * 100);
if (pct > maxScroll) { maxScroll = pct; }
});
window.addEventListener('beforeunload', function() {
if (maxScroll > 0) { send('scroll_depth', JSON.stringify({max_percent: maxScroll})); }
});
})();
</script>
{{end}}
</body>
</html>