website/js/main.js

277 lines
9.2 KiB
JavaScript
Raw Normal View History

2026-04-29 17:09:16 +02:00
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttr(str) {
if (str == null) return '';
return String(str).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}