From e41bd916f7cac129e7a8cca09c3c8450746bc889 Mon Sep 17 00:00:00 2001 From: jae Date: Tue, 31 Mar 2026 21:10:40 +0000 Subject: [PATCH] feat: add individual post page (post.html, post.js, post.css) --- css/post.css | 490 +++++++++++++++++++++++++++++++++++++++++++++++++++ js/post.js | 325 ++++++++++++++++++++++++++++++++++ post.html | 118 +++++++++++++ 3 files changed, 933 insertions(+) create mode 100644 css/post.css create mode 100644 js/post.js create mode 100644 post.html diff --git a/css/post.css b/css/post.css new file mode 100644 index 0000000..f4beb73 --- /dev/null +++ b/css/post.css @@ -0,0 +1,490 @@ +/* =================================================== + JAESWIFT — Individual Post Page Styles + =================================================== */ + +/* ─── Post Header ─── */ +.post-header { + padding: 10rem 2rem 3rem; + position: relative; + border-bottom: 1px solid rgba(0, 255, 200, 0.1); +} + +.post-header-content { + max-width: 1100px; + margin: 0 auto; +} + +.post-back { + display: inline-block; + color: var(--accent, #00ffc8); + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + letter-spacing: 1px; + text-decoration: none; + margin-bottom: 1.5rem; + opacity: 0.6; + transition: opacity 0.3s; +} +.post-back:hover { + opacity: 1; +} + +.post-header-meta { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.post-header .post-date { + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + color: rgba(0, 255, 200, 0.6); + letter-spacing: 1px; +} + +.post-header .post-time { + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: rgba(200, 214, 229, 0.4); +} + +.post-header-title { + font-family: 'Orbitron', sans-serif; + font-size: clamp(1.4rem, 3vw, 2.2rem); + font-weight: 700; + color: #fff; + margin-bottom: 1rem; + line-height: 1.3; +} + +.post-header-tags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.post-header-tags .post-tag { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + padding: 0.25rem 0.6rem; + border: 1px solid rgba(0, 255, 200, 0.2); + color: var(--accent, #00ffc8); + letter-spacing: 1px; + text-transform: uppercase; +} + +/* ─── Threat Badges ─── */ +.post-threat { + font-family: 'JetBrains Mono', monospace; + font-size: 0.7rem; + padding: 0.2rem 0.6rem; + letter-spacing: 1px; + font-weight: 600; +} +.threat-low { + color: #00ffc8; + border: 1px solid rgba(0, 255, 200, 0.3); + text-shadow: 0 0 8px rgba(0, 255, 200, 0.3); +} +.threat-medium, .threat-med { + color: #ffa502; + border: 1px solid rgba(255, 165, 2, 0.3); + text-shadow: 0 0 8px rgba(255, 165, 2, 0.3); +} +.threat-high { + color: #ff4757; + border: 1px solid rgba(255, 71, 87, 0.3); + text-shadow: 0 0 8px rgba(255, 71, 87, 0.3); +} +.threat-critical { + color: #ff0040; + border: 1px solid rgba(255, 0, 64, 0.4); + text-shadow: 0 0 12px rgba(255, 0, 64, 0.4); + animation: threatPulse 1.5s ease-in-out infinite; +} +@keyframes threatPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ─── Post Layout ─── */ +.post-body-section { + padding: 3rem 2rem 5rem; +} + +.post-layout { + max-width: 1100px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 300px; + gap: 2.5rem; + align-items: start; +} + +/* ─── Post Content ─── */ +.post-content-main { + min-width: 0; +} + +.post-rendered { + font-family: 'JetBrains Mono', monospace; + font-size: 0.9rem; + line-height: 1.8; + color: rgba(200, 214, 229, 0.85); +} + +.post-rendered h1, +.post-rendered h2, +.post-rendered h3, +.post-rendered h4 { + font-family: 'Orbitron', sans-serif; + color: #fff; + margin: 2rem 0 1rem; + line-height: 1.3; +} +.post-rendered h1 { font-size: 1.6rem; } +.post-rendered h2 { + font-size: 1.2rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(0, 255, 200, 0.1); +} +.post-rendered h3 { font-size: 1rem; color: var(--accent, #00ffc8); } +.post-rendered h4 { font-size: 0.9rem; } + +.post-rendered p { + margin-bottom: 1.2rem; +} + +.post-rendered strong { + color: #fff; + font-weight: 600; +} + +.post-rendered em { + color: rgba(0, 255, 200, 0.7); + font-style: italic; +} + +.post-rendered a { + color: var(--accent, #00ffc8); + text-decoration: none; + border-bottom: 1px solid rgba(0, 255, 200, 0.3); + transition: border-color 0.3s; +} +.post-rendered a:hover { + border-color: var(--accent, #00ffc8); +} + +.post-rendered ul { + list-style: none; + padding-left: 0; + margin: 1rem 0; +} +.post-rendered ul li { + position: relative; + padding-left: 1.5rem; + margin-bottom: 0.5rem; +} +.post-rendered ul li::before { + content: '▸'; + position: absolute; + left: 0; + color: var(--accent, #00ffc8); + font-size: 0.8rem; +} + +.post-rendered hr { + border: none; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(0, 255, 200, 0.2), transparent); + margin: 2rem 0; +} + +/* ─── Code Blocks ─── */ +.post-rendered pre { + background: rgba(0, 0, 0, 0.5); + border: 1px solid rgba(0, 255, 200, 0.1); + border-left: 3px solid var(--accent, #00ffc8); + border-radius: 0; + padding: 1.2rem 1.5rem; + margin: 1.5rem 0; + overflow-x: auto; + position: relative; +} + +.post-rendered pre::before { + content: attr(data-lang) 'TERMINAL'; + position: absolute; + top: 0; + right: 0; + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + padding: 0.2rem 0.6rem; + background: rgba(0, 255, 200, 0.08); + color: rgba(0, 255, 200, 0.4); + letter-spacing: 1px; +} + +.post-rendered pre code { + font-family: 'Share Tech Mono', 'JetBrains Mono', monospace; + font-size: 0.82rem; + color: var(--accent, #00ffc8); + line-height: 1.6; + background: none; + padding: 0; +} + +.post-rendered code.inline-code { + font-family: 'Share Tech Mono', monospace; + font-size: 0.82rem; + background: rgba(0, 255, 200, 0.06); + border: 1px solid rgba(0, 255, 200, 0.1); + padding: 0.15rem 0.4rem; + color: var(--accent, #00ffc8); +} + +/* ─── Sidebar ─── */ +.post-sidebar { + display: flex; + flex-direction: column; + gap: 1.5rem; + position: sticky; + top: 6rem; +} + +.post-stats-panel, +.post-meta-panel, +.post-nav-panel { + background: rgba(0, 255, 200, 0.02); + border: 1px solid rgba(0, 255, 200, 0.08); +} + +/* ─── Stat Rows (sidebar) ─── */ +.stat-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.4rem 0; +} + +.stat-label { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + color: rgba(200, 214, 229, 0.5); + letter-spacing: 1px; + min-width: 80px; +} + +.stat-pips { + display: flex; + gap: 4px; +} + +.pip { + width: 14px; + height: 8px; + background: rgba(200, 214, 229, 0.08); + border: 1px solid rgba(200, 214, 229, 0.1); + transition: all 0.3s; +} +.pip.filled { + background: var(--accent, #00ffc8); + border-color: var(--accent, #00ffc8); + box-shadow: 0 0 6px rgba(0, 255, 200, 0.3); +} +.pip.filled.warn { + background: #ffa502; + border-color: #ffa502; + box-shadow: 0 0 6px rgba(255, 165, 2, 0.3); +} +.pip.filled.danger { + background: #ff4757; + border-color: #ff4757; + box-shadow: 0 0 6px rgba(255, 71, 87, 0.3); +} + +.stat-divider { + height: 1px; + background: rgba(0, 255, 200, 0.08); + margin: 0.5rem 0; +} + +/* ─── BPM Display ─── */ +.stat-big-row { + text-align: center; + padding: 0.5rem 0; +} + +.stat-bpm-display { + position: relative; +} + +.stat-bpm-value { + font-family: 'Orbitron', sans-serif; + font-size: 2rem; + font-weight: 700; + color: #ff4757; + text-shadow: 0 0 15px rgba(255, 71, 87, 0.4); + transition: transform 0.1s; +} +.stat-bpm-value.pulse { + transform: scale(1.15); +} + +.stat-bpm-unit { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + color: rgba(255, 71, 87, 0.5); + letter-spacing: 2px; + display: block; + margin-top: -0.2rem; +} + +/* ─── Coffee Row ─── */ +.stat-coffee-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.4rem 0; +} + +.stat-coffee { + font-size: 0.9rem; +} + +/* ─── Metadata Panel ─── */ +.meta-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.4rem 0; + border-bottom: 1px solid rgba(0, 255, 200, 0.04); +} +.meta-row:last-child { + border-bottom: none; +} + +.meta-key { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + color: rgba(200, 214, 229, 0.4); + letter-spacing: 1px; +} + +.meta-val { + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: rgba(200, 214, 229, 0.8); + text-align: right; +} +.meta-val.mono { + font-size: 0.65rem; + color: rgba(0, 255, 200, 0.5); +} + +/* ─── Navigation Panel ─── */ +.post-nav-link { + display: block; + padding: 0.8rem 0; + border-bottom: 1px solid rgba(0, 255, 200, 0.06); + text-decoration: none; + transition: all 0.3s; +} +.post-nav-link:last-child { + border-bottom: none; +} +.post-nav-link:hover { + background: rgba(0, 255, 200, 0.03); + padding-left: 0.5rem; +} + +.nav-dir { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + color: var(--accent, #00ffc8); + letter-spacing: 1px; + display: block; + margin-bottom: 0.2rem; +} + +.nav-post-title { + font-family: 'JetBrains Mono', monospace; + font-size: 0.75rem; + color: rgba(200, 214, 229, 0.7); + display: block; + line-height: 1.4; +} + +.nav-all .nav-dir { + text-align: center; + margin-top: 0.3rem; +} + +/* ─── Loading & Error ─── */ +.post-loading { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 4rem 0; + color: rgba(0, 255, 200, 0.5); + font-family: 'JetBrains Mono', monospace; + font-size: 0.8rem; + letter-spacing: 2px; +} + +.post-error { + text-align: center; + padding: 4rem 2rem; + font-family: 'Orbitron', sans-serif; + font-size: 1.1rem; + color: #ff4757; + text-shadow: 0 0 20px rgba(255, 71, 87, 0.3); +} + +/* ─── Responsive ─── */ +@media (max-width: 900px) { + .post-layout { + grid-template-columns: 1fr; + } + + .post-sidebar { + position: static; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + } + + .post-nav-panel { + grid-column: 1 / -1; + } +} + +@media (max-width: 600px) { + .post-header { + padding: 8rem 1rem 2rem; + } + + .post-body-section { + padding: 2rem 1rem 3rem; + } + + .post-header-title { + font-size: 1.2rem; + } + + .post-sidebar { + grid-template-columns: 1fr; + } + + .post-rendered { + font-size: 0.82rem; + } + + .post-rendered pre { + padding: 0.8rem 1rem; + font-size: 0.75rem; + } +} diff --git a/js/post.js b/js/post.js new file mode 100644 index 0000000..046cc39 --- /dev/null +++ b/js/post.js @@ -0,0 +1,325 @@ +/* =================================================== + JAESWIFT BLOG — Individual Post Loader + Markdown rendering, operator stats, navigation + =================================================== */ +(function () { + 'use strict'; + + const API = window.location.hostname === 'localhost' + ? 'http://localhost:5000' + : '/api'; + + // ─── Simple Markdown Parser ─── + function parseMarkdown(md) { + let html = md; + + // Code blocks (```lang ... ```) + html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (_, lang, code) => { + const cls = lang ? ` class="language-${lang}"` : ''; + return `
${escapeHtml(code.trim())}
`; + }); + + // Inline code + html = html.replace(/`([^`]+)`/g, '$1'); + + // Headers + html = html.replace(/^#### (.+)$/gm, '

$1

'); + html = html.replace(/^### (.+)$/gm, '

$1

'); + html = html.replace(/^## (.+)$/gm, '

$1

'); + html = html.replace(/^# (.+)$/gm, '

$1

'); + + // Bold & Italic + html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1'); + html = html.replace(/\*\*(.+?)\*\*/g, '$1'); + html = html.replace(/\*(.+?)\*/g, '$1'); + + // Links + html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + // Unordered lists + html = html.replace(/^- (.+)$/gm, '
  • $1
  • '); + html = html.replace(/(
  • .*<\/li>\n?)+/g, (match) => ``); + + // Horizontal rule + html = html.replace(/^---$/gm, '
    '); + + // Paragraphs (double newline) + html = html.replace(/\n\n(?!<)/g, '

    '); + html = '

    ' + html + '

    '; + + // Clean up empty paragraphs and fix nesting + html = html.replace(/

    <(h[1-4]|pre|ul|hr|blockquote)/g, '<$1'); + html = html.replace(/<\/(h[1-4]|pre|ul|hr|blockquote)><\/p>/g, ''); + html = html.replace(/

    <\/p>/g, ''); + html = html.replace(/

    \s*<\/p>/g, ''); + + return html; + } + + function escapeHtml(str) { + return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + } + + // ─── Stat Pips ─── + function buildPips(val, max) { + max = max || 5; + let html = '

    '; + for (let i = 0; i < max; i++) { + let cls = 'pip'; + if (i < val) { + cls += ' filled'; + if (val <= 2) cls += ' danger'; + else if (val <= 3) cls += ' warn'; + } + html += '
    '; + } + html += '
    '; + return html; + } + + // ─── 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', () => { + const nb = document.getElementById('navbar'); + if (nb) nb.classList.toggle('scrolled', window.scrollY > 50); + }, { passive: true }); + } + + // ─── Render Operator Stats ─── + function renderStats(post) { + const el = document.getElementById('postStats'); + if (!el) return; + + const coffee = '☕'.repeat(post.coffee || 0) + + '' + '☕'.repeat(5 - (post.coffee || 0)) + ''; + + el.innerHTML = ` +
    + MOOD + ${buildPips(post.mood)} +
    +
    + ENERGY + ${buildPips(post.energy)} +
    +
    + MOTIVATION + ${buildPips(post.motivation)} +
    +
    + FOCUS + ${buildPips(post.focus)} +
    +
    + DIFFICULTY + ${buildPips(post.difficulty)} +
    +
    +
    +
    + ${post.heart_rate || '---'} + BPM +
    +
    +
    +
    + CAFFEINE + ${coffee} +
    + `; + + // Animate heartbeat + animateHeartbeat(post.heart_rate || 72); + } + + // ─── Heartbeat Animation ─── + function animateHeartbeat(bpm) { + const el = document.getElementById('bpmValue'); + if (!el) return; + const interval = 60000 / bpm; + + setInterval(() => { + el.classList.add('pulse'); + setTimeout(() => el.classList.remove('pulse'), 200); + // Flicker the value slightly + const jitter = Math.floor(Math.random() * 5) - 2; + el.textContent = bpm + jitter; + }, interval); + } + + // ─── Render Metadata Panel ─── + function renderMetaPanel(post) { + const el = document.getElementById('postMetaPanel'); + if (!el) return; + + el.innerHTML = ` +
    + DATE + ${post.date} +
    +
    + TIME + ${post.time_written || 'UNKNOWN'} +
    +
    + WORDS + ${post.word_count || '---'} +
    +
    + THREAT + ${post.threat_level || 'LOW'} +
    +
    + READ TIME + ~${Math.max(1, Math.ceil((post.word_count || 300) / 250))} MIN +
    +
    + SLUG + ${post.slug} +
    + `; + } + + // ─── Render Navigation ─── + function renderNav(allPosts, currentSlug) { + const el = document.getElementById('postNav'); + if (!el) return; + + // Sort by date descending + const sorted = [...allPosts].sort((a, b) => new Date(b.date) - new Date(a.date)); + const idx = sorted.findIndex(p => p.slug === currentSlug); + + let navHtml = ''; + + if (idx > 0) { + const prev = sorted[idx - 1]; + navHtml += ` + ← NEWER + ${prev.title} + `; + } + + if (idx < sorted.length - 1) { + const next = sorted[idx + 1]; + navHtml += ` + OLDER → + ${next.title} + `; + } + + navHtml += ` + ⟐ ALL TRANSMISSIONS + `; + + el.innerHTML = navHtml; + } + + // ─── Load Post ─── + async function loadPost() { + const params = new URLSearchParams(window.location.search); + const slug = params.get('slug'); + + if (!slug) { + document.getElementById('postContent').innerHTML = + '
    NO TRANSMISSION ID SPECIFIED
    '; + return; + } + + try { + // Fetch the specific post + const res = await fetch(API + '/posts/' + encodeURIComponent(slug)); + if (!res.ok) throw new Error('Post not found'); + const post = await res.json(); + + // Update page title + document.title = 'JAESWIFT // ' + post.title.toUpperCase(); + + // Update header + document.getElementById('postTitle').textContent = post.title; + document.getElementById('postDate').textContent = post.date; + document.getElementById('postTime').textContent = post.time_written || ''; + + const threatEl = document.getElementById('postThreat'); + threatEl.textContent = post.threat_level || 'LOW'; + threatEl.className = 'post-threat threat-' + (post.threat_level || 'LOW').toLowerCase(); + + // Tags + const tagsEl = document.getElementById('postTags'); + tagsEl.innerHTML = (post.tags || []).map(t => + '' + ).join(''); + + // Render content + const contentEl = document.getElementById('postContent'); + contentEl.innerHTML = '
    ' + parseMarkdown(post.content || '') + '
    '; + + // Animate content in + contentEl.style.opacity = '0'; + contentEl.style.transform = 'translateY(20px)'; + contentEl.style.transition = 'all 0.6s ease'; + requestAnimationFrame(() => { + contentEl.style.opacity = '1'; + contentEl.style.transform = 'translateY(0)'; + }); + + // Sidebar panels + renderStats(post); + renderMetaPanel(post); + + // Load all posts for navigation + try { + const allRes = await fetch(API + '/posts'); + const allPosts = await allRes.json(); + renderNav(allPosts, slug); + } catch (e) { + console.warn('Could not load post navigation'); + } + + } catch (err) { + // Fallback: try static JSON + try { + const res2 = await fetch('api/data/posts.json'); + const posts = await res2.json(); + const post = posts.find(p => p.slug === slug); + if (!post) throw new Error('Not found'); + + document.title = 'JAESWIFT // ' + post.title.toUpperCase(); + document.getElementById('postTitle').textContent = post.title; + document.getElementById('postDate').textContent = post.date; + document.getElementById('postTime').textContent = post.time_written || ''; + document.getElementById('postContent').innerHTML = + '
    ' + parseMarkdown(post.content || '') + '
    '; + renderStats(post); + renderMetaPanel(post); + renderNav(posts, slug); + } catch (e2) { + document.getElementById('postContent').innerHTML = + '
    TRANSMISSION NOT FOUND — SIGNAL LOST
    '; + } + } + } + + // ─── Init ─── + document.addEventListener('DOMContentLoaded', () => { + initClock(); + initNavbar(); + loadPost(); + }); +})(); diff --git a/post.html b/post.html new file mode 100644 index 0000000..f618700 --- /dev/null +++ b/post.html @@ -0,0 +1,118 @@ + + + + + + JAESWIFT // POST + + + + + + + + +
    +
    + + + + + +
    +
    + ← BACK TO TRANSMISSIONS +
    + + --- + +
    +

    LOADING TRANSMISSION...

    +
    +
    +
    + + +
    +
    + +
    +
    +
    + DECRYPTING TRANSMISSION... +
    +
    + + + +
    +
    + + +
    + +
    + + + +