- Fix admin login: ID mismatch (loginUser->loginUsername, loginPass->loginPassword) - Fix auth URL: /api/auth -> /api/auth/login - Fix init() structure: broken if/else for checkAuth - Remove onclick from login button (JS handles via addEventListener) - Clean URLs: /blog/post/slug instead of post.html?slug=slug - Updated blog.js, post.js, main.js with clean URL links - post.js supports both /blog/post/slug and ?slug=x formats - nginx configured for /blog/post/* rewrite
185 lines
7.2 KiB
JavaScript
185 lines
7.2 KiB
JavaScript
/* ===================================================
|
|
JAESWIFT BLOG — Post loader + Cyberpunk stats
|
|
=================================================== */
|
|
(function () {
|
|
'use strict';
|
|
|
|
const API = window.location.hostname === 'localhost'
|
|
? 'http://localhost:5000'
|
|
: '/api';
|
|
|
|
// ─── Clock ───
|
|
function initClock() {
|
|
const el = document.getElementById('navClock');
|
|
if (!el) return;
|
|
const tick = () => {
|
|
const d = new Date();
|
|
el.textContent = d.toLocaleTimeString('en-GB', { hour12: false }) + ' UTC' + (d.getTimezoneOffset() <= 0 ? '+' : '') + (-d.getTimezoneOffset() / 60);
|
|
};
|
|
tick();
|
|
setInterval(tick, 1000);
|
|
}
|
|
|
|
// ─── Navbar ───
|
|
function initNavbar() {
|
|
const toggle = document.getElementById('navToggle');
|
|
const menu = document.getElementById('navMenu');
|
|
if (toggle && menu) {
|
|
toggle.addEventListener('click', () => menu.classList.toggle('active'));
|
|
}
|
|
window.addEventListener('scroll', () => {
|
|
document.getElementById('navbar')?.classList.toggle('scrolled', window.scrollY > 50);
|
|
}, { passive: true });
|
|
}
|
|
|
|
// ─── Build Stat Pips ───
|
|
function buildPips(val, max = 5) {
|
|
let html = '<div class="stat-bar-container">';
|
|
for (let i = 0; i < max; i++) {
|
|
const filled = i < val;
|
|
let cls = 'stat-pip';
|
|
if (filled) {
|
|
cls += ' filled';
|
|
if (val <= 2) cls += ' danger';
|
|
else if (val <= 3) cls += ' warn';
|
|
}
|
|
html += `<div class="${cls}"></div>`;
|
|
}
|
|
html += '</div>';
|
|
return html;
|
|
}
|
|
|
|
// ─── Coffee Icons ───
|
|
function buildCoffee(val) {
|
|
return '<span class="stat-coffee">' + '☕'.repeat(val) + '<span style="opacity:0.2">' + '☕'.repeat(5 - val) + '</span></span>';
|
|
}
|
|
|
|
// ─── Render Post Card ───
|
|
function renderPostCard(post) {
|
|
return `
|
|
<article class="post-card" onclick="window.location='/blog/post/${post.slug}'" data-tags="${(post.tags || []).join(',')}">
|
|
<div class="post-card-content">
|
|
<div class="post-card-meta">
|
|
<span class="post-date">${post.date}</span>
|
|
<span class="post-threat threat-${post.threat_level}">${post.threat_level}</span>
|
|
<span class="post-time">${post.time_written || ''}</span>
|
|
</div>
|
|
<h2 class="post-card-title">${post.title}</h2>
|
|
<p class="post-card-excerpt">${post.excerpt}</p>
|
|
<div class="post-card-tags">
|
|
${(post.tags || []).map(t => `<span class="post-tag">${t}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="post-card-stats">
|
|
<div class="stats-header">OPERATOR STATUS</div>
|
|
<div class="stat-row">
|
|
<span class="stat-row-label">MOOD</span>
|
|
${buildPips(post.mood)}
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-row-label">ENERGY</span>
|
|
${buildPips(post.energy)}
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-row-label">MOTIVE</span>
|
|
${buildPips(post.motivation)}
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-row-label">FOCUS</span>
|
|
${buildPips(post.focus)}
|
|
</div>
|
|
<div style="text-align:center; margin-top:0.25rem;">
|
|
<div class="stat-bpm">${post.heart_rate} <span class="stat-bpm-label">BPM</span></div>
|
|
</div>
|
|
<div style="text-align:center;">
|
|
${buildCoffee(post.coffee)}
|
|
</div>
|
|
</div>
|
|
</article>`;
|
|
}
|
|
|
|
// ─── Load Posts ───
|
|
async function loadPosts() {
|
|
const grid = document.getElementById('blogPosts');
|
|
if (!grid) return;
|
|
|
|
try {
|
|
const res = await fetch(API + '/posts');
|
|
if (!res.ok) throw new Error('API error');
|
|
const posts = await res.json();
|
|
|
|
if (posts.length === 0) {
|
|
grid.innerHTML = '<div class="blog-loading">NO TRANSMISSIONS FOUND</div>';
|
|
return;
|
|
}
|
|
|
|
// Sort by date descending
|
|
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
grid.innerHTML = posts.map(renderPostCard).join('');
|
|
|
|
// Animate cards in
|
|
const cards = grid.querySelectorAll('.post-card');
|
|
cards.forEach((card, i) => {
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'translateY(20px)';
|
|
card.style.transition = 'all 0.5s ease';
|
|
setTimeout(() => {
|
|
card.style.opacity = '1';
|
|
card.style.transform = 'translateY(0)';
|
|
}, 100 + i * 150);
|
|
});
|
|
|
|
} catch (err) {
|
|
// Fallback: load from static JSON
|
|
try {
|
|
const res2 = await fetch('api/data/posts.json');
|
|
const posts = await res2.json();
|
|
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
grid.innerHTML = posts.map(renderPostCard).join('');
|
|
const cards = grid.querySelectorAll('.post-card');
|
|
cards.forEach((card, i) => {
|
|
card.style.opacity = '0';
|
|
card.style.transform = 'translateY(20px)';
|
|
card.style.transition = 'all 0.5s ease';
|
|
setTimeout(() => {
|
|
card.style.opacity = '1';
|
|
card.style.transform = 'translateY(0)';
|
|
}, 100 + i * 150);
|
|
});
|
|
} catch (e2) {
|
|
grid.innerHTML = '<div class="blog-loading">TRANSMISSION ERROR — RETRY LATER</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Filters ───
|
|
function initFilters() {
|
|
const btns = document.querySelectorAll('.filter-btn');
|
|
btns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
btns.forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
const filter = btn.dataset.filter;
|
|
const cards = document.querySelectorAll('.post-card');
|
|
cards.forEach(card => {
|
|
const tags = card.dataset.tags || '';
|
|
if (filter === 'all' || tags.includes(filter)) {
|
|
card.style.display = '';
|
|
card.style.opacity = '1';
|
|
} else {
|
|
card.style.opacity = '0';
|
|
setTimeout(() => { card.style.display = 'none'; }, 300);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── Init ───
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initClock();
|
|
initNavbar();
|
|
loadPosts();
|
|
initFilters();
|
|
});
|
|
})();
|