277 lines
9.2 KiB
JavaScript
277 lines
9.2 KiB
JavaScript
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
loadComponents().then(() => {
|
||
|
|
initSkillIssueTicker();
|
||
|
|
initCryptoCopier();
|
||
|
|
});
|
||
|
|
|
||
|
|
loadRecentPosts();
|
||
|
|
initTypingEffect();
|
||
|
|
initScrollReveal();
|
||
|
|
initScrollParallax();
|
||
|
|
|
||
|
|
if (document.getElementById('sec-grid')) {
|
||
|
|
initSearchableGrid('sec', 'sec-grid', 'sec-search');
|
||
|
|
}
|
||
|
|
if (document.getElementById('magic-grid')) {
|
||
|
|
initSearchableGrid('magic', 'magic-grid', 'magic-search');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
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}}', '');
|
||
|
|
const headerEl = document.querySelector('header');
|
||
|
|
if (headerEl) headerEl.innerHTML = headerHtml;
|
||
|
|
}
|
||
|
|
|
||
|
|
const footerRes = await fetch('components/footer.html');
|
||
|
|
if (footerRes.ok) {
|
||
|
|
const footerEl = document.querySelector('footer');
|
||
|
|
if (footerEl) footerEl.innerHTML = await footerRes.text();
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Component load error:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function initScrollReveal() {
|
||
|
|
const sections = document.querySelectorAll('main > section');
|
||
|
|
sections.forEach(section => {
|
||
|
|
if (!section.classList.contains('hero')) {
|
||
|
|
section.classList.add('reveal');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
entry.target.classList.add('in-view');
|
||
|
|
observer.unobserve(entry.target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, { threshold: 0.12, rootMargin: '0px 0px -60px 0px' });
|
||
|
|
|
||
|
|
document.querySelectorAll('.reveal, .reveal-stagger, main > section').forEach(el => {
|
||
|
|
observer.observe(el);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function attachStaggerObserver(root) {
|
||
|
|
if (!root) return;
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
entry.target.classList.add('in-view');
|
||
|
|
observer.unobserve(entry.target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
||
|
|
observer.observe(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
function initTypingEffect() {
|
||
|
|
const el = document.querySelector('.hero-typed');
|
||
|
|
if (!el) return;
|
||
|
|
|
||
|
|
const text = "My Computer Science notes, mostly focused on trying to master cybersecurity.";
|
||
|
|
let index = 0;
|
||
|
|
|
||
|
|
function type() {
|
||
|
|
if (index < text.length) {
|
||
|
|
el.textContent += text[index];
|
||
|
|
index++;
|
||
|
|
setTimeout(type, 28 + Math.random() * 35);
|
||
|
|
} else {
|
||
|
|
el.classList.add('done');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setTimeout(type, 700);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadRecentPosts() {
|
||
|
|
const container = document.getElementById('post-container');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/collections/cases/records?sort=-created');
|
||
|
|
if (!response.ok) throw new Error('Status: ' + response.status);
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
const posts = (data.items || []).slice(0, 6);
|
||
|
|
|
||
|
|
if (posts.length === 0) {
|
||
|
|
container.innerHTML = '<p class="empty-state mono">No posts yet.</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.classList.add('reveal-stagger');
|
||
|
|
container.innerHTML = posts.map(post => {
|
||
|
|
const linkUrl = 'walkthroughs/view.html?id=' + post.id;
|
||
|
|
const tagStyle = post.category === 'magic'
|
||
|
|
? 'color: #a855f7; background: rgba(168, 85, 247, 0.1);'
|
||
|
|
: '';
|
||
|
|
const date = (post.created || '').split(' ')[0];
|
||
|
|
return `
|
||
|
|
<a href="${linkUrl}" class="post-item">
|
||
|
|
<span class="post-meta mono">
|
||
|
|
<span class="post-tag" style="${tagStyle}">[${escapeHtml(post.tag || '')}]</span>
|
||
|
|
${escapeHtml(date)}
|
||
|
|
</span>
|
||
|
|
<h3>${escapeHtml(post.title || '')}</h3>
|
||
|
|
<p>${escapeHtml(post.summary || '')}</p>
|
||
|
|
</a>`;
|
||
|
|
}).join('');
|
||
|
|
|
||
|
|
attachStaggerObserver(container);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to sync data:', e);
|
||
|
|
container.innerHTML = '<p class="empty-state mono">Failed to load posts.</p>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function initSearchableGrid(category, gridId, searchId) {
|
||
|
|
const grid = document.getElementById(gridId);
|
||
|
|
const searchInput = document.getElementById(searchId);
|
||
|
|
if (!grid) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/collections/cases/records?sort=-created');
|
||
|
|
if (!response.ok) throw new Error('Status: ' + response.status);
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
const allPosts = data.items || [];
|
||
|
|
const categoryPosts = allPosts.filter(p => p.category === category);
|
||
|
|
|
||
|
|
const render = (items) => {
|
||
|
|
grid.classList.add('reveal-stagger');
|
||
|
|
grid.classList.remove('in-view');
|
||
|
|
|
||
|
|
if (items.length === 0) {
|
||
|
|
grid.classList.remove('reveal-stagger');
|
||
|
|
grid.innerHTML = '<p class="empty-state mono">No results found.</p>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
grid.innerHTML = items.map(post => {
|
||
|
|
let imageHtml = '';
|
||
|
|
let cardClass = 'sec-card';
|
||
|
|
const date = (post.created || '').split(' ')[0];
|
||
|
|
|
||
|
|
if (category === 'magic' && post.thumbnail) {
|
||
|
|
imageHtml = `<div class="card-img" style="background-image: url('${escapeAttr(post.thumbnail)}');"></div>`;
|
||
|
|
cardClass += ' has-image';
|
||
|
|
}
|
||
|
|
|
||
|
|
return `
|
||
|
|
<a href="walkthroughs/view.html?id=${encodeURIComponent(post.id)}" class="${cardClass}">
|
||
|
|
${imageHtml}
|
||
|
|
<div class="card-content">
|
||
|
|
<h3 class="mono">${escapeHtml(post.title || '')}</h3>
|
||
|
|
<span class="card-meta mono">[${escapeHtml(post.tag || '')}] ${escapeHtml(date)}</span>
|
||
|
|
</div>
|
||
|
|
</a>`;
|
||
|
|
}).join('');
|
||
|
|
|
||
|
|
requestAnimationFrame(() => grid.classList.add('in-view'));
|
||
|
|
};
|
||
|
|
|
||
|
|
render(categoryPosts);
|
||
|
|
|
||
|
|
if (searchInput) {
|
||
|
|
searchInput.addEventListener('input', (e) => {
|
||
|
|
const term = e.target.value.toLowerCase();
|
||
|
|
const filtered = categoryPosts.filter(post =>
|
||
|
|
(post.title || '').toLowerCase().includes(term) ||
|
||
|
|
(post.tag || '').toLowerCase().includes(term) ||
|
||
|
|
(post.summary || '').toLowerCase().includes(term)
|
||
|
|
);
|
||
|
|
render(filtered);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Grid error:', e);
|
||
|
|
grid.innerHTML = '<p class="empty-state mono">Failed to load.</p>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function initSkillIssueTicker() {
|
||
|
|
const tickerContainer = document.getElementById('skill-issue-feed');
|
||
|
|
if (!tickerContainer) return;
|
||
|
|
|
||
|
|
const messages = [
|
||
|
|
"Zero Days found: 0",
|
||
|
|
"Imposter Syndrome kicking in",
|
||
|
|
"Pigeons are liars",
|
||
|
|
"CERTIFICATIONS: Trust me bro",
|
||
|
|
"The S in IOT stand for Security.",
|
||
|
|
"Must be a layer 8 issue.",
|
||
|
|
"The best way to secure a computer is to turn it off and never power it on.",
|
||
|
|
"And then?"
|
||
|
|
];
|
||
|
|
|
||
|
|
const content = [...messages, ...messages, ...messages]
|
||
|
|
.map(msg => `<span class="ticker-item">${escapeHtml(msg)}</span>`)
|
||
|
|
.join('');
|
||
|
|
tickerContainer.innerHTML = content;
|
||
|
|
}
|
||
|
|
|
||
|
|
function initCryptoCopier() {
|
||
|
|
document.querySelectorAll('.crypto-addr').forEach(addr => {
|
||
|
|
addr.addEventListener('click', async () => {
|
||
|
|
const originalText = addr.innerText;
|
||
|
|
const cleanAddress = originalText.replace(/\s/g, '');
|
||
|
|
try {
|
||
|
|
await navigator.clipboard.writeText(cleanAddress);
|
||
|
|
addr.classList.add('copied');
|
||
|
|
addr.innerText = '[ ADDRESS COPIED ]';
|
||
|
|
setTimeout(() => {
|
||
|
|
addr.classList.remove('copied');
|
||
|
|
addr.innerText = originalText;
|
||
|
|
}, 1500);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Copy failed:', err);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
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, ''');
|
||
|
|
}
|