252 lines
9 KiB
JavaScript
252 lines
9 KiB
JavaScript
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
||
|
|
await loadComponents();
|
||
|
|
initScrollParallax();
|
||
|
|
initLightbox();
|
||
|
|
|
||
|
|
const urlParams = new URLSearchParams(window.location.search);
|
||
|
|
const caseId = urlParams.get('id');
|
||
|
|
|
||
|
|
if (!caseId) {
|
||
|
|
document.getElementById('main-content').innerHTML =
|
||
|
|
'<div class="container"><h2>Error: No case ID specified.</h2></div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await loadCaseData(caseId);
|
||
|
|
attachLightboxToImages();
|
||
|
|
});
|
||
|
|
|
||
|
|
function initScrollParallax() {
|
||
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
|
|
const root = document.documentElement;
|
||
|
|
let scrollY = 0;
|
||
|
|
let ticking = false;
|
||
|
|
function update() {
|
||
|
|
root.style.setProperty('--scroll', scrollY);
|
||
|
|
ticking = false;
|
||
|
|
}
|
||
|
|
window.addEventListener('scroll', () => {
|
||
|
|
scrollY = window.scrollY || window.pageYOffset || 0;
|
||
|
|
if (!ticking) {
|
||
|
|
requestAnimationFrame(update);
|
||
|
|
ticking = true;
|
||
|
|
}
|
||
|
|
}, { passive: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadComponents() {
|
||
|
|
try {
|
||
|
|
const headerRes = await fetch('../components/header.html');
|
||
|
|
if (headerRes.ok) {
|
||
|
|
let headerHtml = await headerRes.text();
|
||
|
|
headerHtml = headerHtml.replaceAll('{{prefix}}', '../');
|
||
|
|
document.querySelector('header').innerHTML = headerHtml;
|
||
|
|
}
|
||
|
|
const footerRes = await fetch('../components/footer.html');
|
||
|
|
if (footerRes.ok) {
|
||
|
|
document.querySelector('footer').innerHTML = await footerRes.text();
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Component load error', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadCaseData(id) {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/collections/cases/records');
|
||
|
|
const data = await response.json();
|
||
|
|
const cases = data.items || [];
|
||
|
|
const currentCase = cases.find(c => c.id === id);
|
||
|
|
|
||
|
|
if (!currentCase) {
|
||
|
|
document.getElementById('main-content').innerHTML =
|
||
|
|
'<div class="container"><h2>Error: Case not found.</h2></div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const backLink = document.getElementById('back-link');
|
||
|
|
if (backLink) {
|
||
|
|
if (currentCase.category === 'magic') {
|
||
|
|
backLink.href = '../magic.html';
|
||
|
|
backLink.innerHTML = '← BACK TO MAGIC';
|
||
|
|
} else {
|
||
|
|
backLink.href = '../sec.html';
|
||
|
|
backLink.innerHTML = '← BACK TO SEC';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const titleElement = document.getElementById('case-title');
|
||
|
|
titleElement.innerText = currentCase.title || '';
|
||
|
|
|
||
|
|
if (currentCase.category === 'magic') {
|
||
|
|
titleElement.classList.add('expert-title');
|
||
|
|
titleElement.classList.remove('text-gradient');
|
||
|
|
}
|
||
|
|
|
||
|
|
document.title = (currentCase.title || 'Case Study') + ' | t3jfel SEC';
|
||
|
|
document.getElementById('case-tag').innerText = currentCase.tag || '';
|
||
|
|
const date = (currentCase.created || '').split(' ')[0];
|
||
|
|
document.getElementById('case-meta').innerText =
|
||
|
|
'DATE: ' + date + ' | ID: ' + currentCase.id;
|
||
|
|
document.getElementById('case-intro').innerText = currentCase.intro || '';
|
||
|
|
|
||
|
|
const container = document.getElementById('steps-container');
|
||
|
|
container.innerHTML = '';
|
||
|
|
|
||
|
|
if (currentCase.category === 'magic' && currentCase.sections) {
|
||
|
|
container.className = 'expert-container';
|
||
|
|
currentCase.sections.forEach(section => {
|
||
|
|
let sectionHtml =
|
||
|
|
'<div class="expert-section">' +
|
||
|
|
'<h2 class="expert-h2">' + escapeHtml(section.heading || '') + '</h2>';
|
||
|
|
|
||
|
|
(section.content || []).forEach(block => {
|
||
|
|
if (block.type === 'text') {
|
||
|
|
sectionHtml += '<p class="expert-p">' + escapeHtml(block.value) + '</p>';
|
||
|
|
} else if (block.type === 'code') {
|
||
|
|
const codeVal = Array.isArray(block.value) ? block.value.join('\n') : block.value;
|
||
|
|
sectionHtml +=
|
||
|
|
'<pre class="expert-code-block"><code>' +
|
||
|
|
escapeHtml(codeVal) + '</code></pre>';
|
||
|
|
} else if (block.type === 'image') {
|
||
|
|
const imgPath = block.value.startsWith('http') ? block.value : '../' + block.value;
|
||
|
|
sectionHtml +=
|
||
|
|
'<div class="expert-image-wrapper">' +
|
||
|
|
'<img src="' + escapeAttr(imgPath) + '" alt="Documentation Image" class="lightbox-trigger">' +
|
||
|
|
'</div>';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
sectionHtml += '</div>';
|
||
|
|
container.insertAdjacentHTML('beforeend', sectionHtml);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
container.className = 'analysis-steps-container';
|
||
|
|
(currentCase.steps || []).forEach((step, idx) => {
|
||
|
|
let bodyHtml = '';
|
||
|
|
if (step.body) {
|
||
|
|
step.body.forEach(block => {
|
||
|
|
if (block.type === 'text') {
|
||
|
|
bodyHtml += '<p>' + escapeHtml(block.value) + '</p>';
|
||
|
|
} else if (block.type === 'code') {
|
||
|
|
const codeVal = Array.isArray(block.value) ? block.value.join('\n') : block.value;
|
||
|
|
bodyHtml += '<pre><code>' + escapeHtml(codeVal) + '</code></pre>';
|
||
|
|
} else if (block.type === 'image') {
|
||
|
|
const imgPath = block.value.startsWith('http') ? block.value : '../' + block.value;
|
||
|
|
bodyHtml += '<img src="' + escapeAttr(imgPath) + '" alt="Step image" class="lightbox-trigger">';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const num = String(idx + 1).padStart(2, '0');
|
||
|
|
const stepHtml =
|
||
|
|
'<article class="blog-step">' +
|
||
|
|
'<div class="step-header">' +
|
||
|
|
'<span class="step-number mono">Step ' + num + '</span>' +
|
||
|
|
'<h2 class="step-title">' + escapeHtml(step.title || '') + '</h2>' +
|
||
|
|
'</div>' +
|
||
|
|
'<div class="step-body">' + bodyHtml + '</div>' +
|
||
|
|
'</article>';
|
||
|
|
container.insertAdjacentHTML('beforeend', stepHtml);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var lb = { scale: 1, tx: 0, ty: 0 };
|
||
|
|
|
||
|
|
function initLightbox() {
|
||
|
|
const overlay = document.createElement('div');
|
||
|
|
overlay.id = 'lightbox-overlay';
|
||
|
|
overlay.innerHTML =
|
||
|
|
'<div id="lightbox-content">' +
|
||
|
|
'<img id="lightbox-img" src="" alt="Enlarged image">' +
|
||
|
|
'</div>' +
|
||
|
|
'<button id="lightbox-close" aria-label="Close">×</button>';
|
||
|
|
document.body.appendChild(overlay);
|
||
|
|
|
||
|
|
const img = document.getElementById('lightbox-img');
|
||
|
|
|
||
|
|
function applyTransform() {
|
||
|
|
img.style.transform =
|
||
|
|
'translate(' + lb.tx + 'px, ' + lb.ty + 'px) scale(' + lb.scale + ')';
|
||
|
|
}
|
||
|
|
|
||
|
|
overlay.addEventListener('click', function (e) {
|
||
|
|
if (e.target === overlay || e.target.id === 'lightbox-content') {
|
||
|
|
closeLightbox();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.getElementById('lightbox-close').addEventListener('click', closeLightbox);
|
||
|
|
|
||
|
|
overlay.addEventListener('wheel', function (e) {
|
||
|
|
e.preventDefault();
|
||
|
|
|
||
|
|
const rect = img.getBoundingClientRect();
|
||
|
|
const dx = e.clientX - rect.left;
|
||
|
|
const dy = e.clientY - rect.top;
|
||
|
|
|
||
|
|
const factor = e.deltaY < 0 ? 1.12 : 0.9;
|
||
|
|
const newScale = Math.min(Math.max(lb.scale * factor, 0.15), 10);
|
||
|
|
const ratio = newScale / lb.scale;
|
||
|
|
|
||
|
|
lb.tx = lb.tx + dx * (1 - ratio);
|
||
|
|
lb.ty = lb.ty + dy * (1 - ratio);
|
||
|
|
lb.scale = newScale;
|
||
|
|
|
||
|
|
applyTransform();
|
||
|
|
}, { passive: false });
|
||
|
|
|
||
|
|
document.addEventListener('keydown', function (e) {
|
||
|
|
if (overlay.classList.contains('active') && e.key === 'Escape') {
|
||
|
|
closeLightbox();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function openLightbox(src) {
|
||
|
|
lb.scale = 1;
|
||
|
|
lb.tx = 0;
|
||
|
|
lb.ty = 0;
|
||
|
|
|
||
|
|
const img = document.getElementById('lightbox-img');
|
||
|
|
img.src = src;
|
||
|
|
img.style.transform = '';
|
||
|
|
|
||
|
|
document.getElementById('lightbox-overlay').classList.add('active');
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeLightbox() {
|
||
|
|
document.getElementById('lightbox-overlay').classList.remove('active');
|
||
|
|
document.body.style.overflow = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
function attachLightboxToImages() {
|
||
|
|
document.querySelectorAll('.lightbox-trigger').forEach(function (img) {
|
||
|
|
img.addEventListener('click', function () {
|
||
|
|
openLightbox(img.src);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function escapeHtml(str) {
|
||
|
|
if (str == null) return '';
|
||
|
|
return String(str)
|
||
|
|
.replace(/&/g, '&')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, ''');
|
||
|
|
}
|
||
|
|
|
||
|
|
function escapeAttr(str) {
|
||
|
|
if (str == null) return '';
|
||
|
|
return String(str).replace(/"/g, '"').replace(/'/g, ''');
|
||
|
|
}
|
||
|
|
|