Initial Release

This commit is contained in:
2026-05-24 19:52:45 -07:00
commit 8242fdbbb3
181 changed files with 10119 additions and 0 deletions
@@ -0,0 +1,357 @@
document.querySelector("div.layout-background").appendChild((() => {
const COLOR_PARTICLE = 0x363636
const COLOR_FOREGROUND = 0x282828
const COLOR_BACKGROUND = 0x000000
const PARTICLE_COUNT = 320
const FRAME_TIME = 1000 / 15
let gl, canvas
let planeProg, particleProg
let planeBuf, planeIdxBuf, planeOrigBuf
let particlePosBuf, particleColorBuf
let planeVerts, planeOrig, planeIdx
let particlePos, particleColor, particleData = []
let lastTime = 0, lastFrame = 0, time = 0
let planeW, planeH, planeSegX, planeSegY
let uTimePlane, uMVPPlane, uFogColorPlane, uFogNearPlane, uFogFarPlane
let uMVPParticle, uFogColorParticle, uFogNearParticle, uFogFarParticle
// --- Math helpers ---
function mat4() { return new Float32Array(16) }
function mat4Identity(m) {
m[0] = 1; m[1] = 0; m[2] = 0; m[3] = 0;
m[4] = 0; m[5] = 1; m[6] = 0; m[7] = 0;
m[8] = 0; m[9] = 0; m[10] = 1; m[11] = 0;
m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1;
return m
}
function mat4Multiply(out, a, b) {
for (let i = 0; i < 4; i++)
for (let j = 0; j < 4; j++) {
out[j * 4 + i] = 0
for (let k = 0; k < 4; k++) out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]
}
return out
}
function mat4Perspective(m, fovY, aspect, near, far) {
const f = 1.0 / Math.tan(fovY / 2)
m[0] = f / aspect; m[1] = 0; m[2] = 0; m[3] = 0
m[4] = 0; m[5] = f; m[6] = 0; m[7] = 0
m[8] = 0; m[9] = 0; m[10] = (far + near) / (near - far); m[11] = -1
m[12] = 0; m[13] = 0; m[14] = (2 * far * near) / (near - far); m[15] = 0
return m
}
function mat4RotateX(m, angle) {
const c = Math.cos(angle), s = Math.sin(angle)
const t = mat4Identity(mat4())
t[5] = c; t[6] = s; t[9] = -s; t[10] = c
return mat4Multiply(mat4(), t, m)
}
function mat4Translate(m, x, y, z) {
const t = mat4Identity(mat4())
t[12] = x; t[13] = y; t[14] = z
return mat4Multiply(mat4(), t, m)
}
function randFloat(lo, hi) { return lo + Math.random() * (hi - lo) }
function randFloatSpread(range) { return randFloat(-range / 2, range / 2) }
function degToRad(d) { return d * Math.PI / 180 }
function intToRGB(i) {
return [
((i >> 16) & 0xFF) / 255,
((i >> 8) & 0xFF) / 255,
((i >> 0) & 0xFF) / 255
]
}
// --- Shader sources ---
const PLANE_VERT = `
attribute vec3 aPosition;
uniform mat4 uMVP;
uniform float uTime;
varying float vDist;
void main() {
float dist = sqrt(aPosition.x*aPosition.x + aPosition.z*aPosition.z);
float wave = sin(dist * 0.5 - uTime);
float cave = -exp(-dist * 0.1) * 3.5;
vec3 pos = vec3(aPosition.x, aPosition.y + wave + cave, aPosition.z);
vDist = length((uMVP * vec4(pos, 1.0)).xyz);
gl_Position = uMVP * vec4(pos, 1.0);
}
`
const PLANE_FRAG = `
precision mediump float;
uniform vec3 uFogColor;
uniform float uFogNear;
uniform float uFogFar;
varying float vDist;
void main() {
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
vec3 color = mix(vec3(${intToRGB(COLOR_FOREGROUND).join(',')}), uFogColor, fog);
gl_FragColor = vec4(color, 1.0);
}
`
const PARTICLE_VERT = `
attribute vec3 aPosition;
attribute vec4 aColor;
uniform mat4 uMVP;
varying vec4 vColor;
varying float vDist;
void main() {
vec4 pos = uMVP * vec4(aPosition, 1.0);
vDist = length(pos.xyz);
vColor = aColor;
gl_PointSize = 3.0;
gl_Position = pos;
}
`
const PARTICLE_FRAG = `
precision mediump float;
uniform vec3 uFogColor;
uniform float uFogNear;
uniform float uFogFar;
varying vec4 vColor;
varying float vDist;
void main() {
float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
float alpha = vColor.a * (1.0 - fog);
gl_FragColor = vec4(mix(vColor.rgb, uFogColor, fog), alpha);
}
`
function compileShader(src, type) {
const s = gl.createShader(type)
gl.shaderSource(s, src)
gl.compileShader(s)
return s
}
function createProgram(vert, frag) {
const p = gl.createProgram()
gl.attachShader(p, compileShader(vert, gl.VERTEX_SHADER))
gl.attachShader(p, compileShader(frag, gl.FRAGMENT_SHADER))
gl.linkProgram(p)
return p
}
function buildPlane(ratio) {
const scale = 24
planeSegX = Math.round(12 * ratio)
planeSegY = 12
planeW = scale * ratio
planeH = scale
const nx = planeSegX + 1, ny = planeSegY + 1
planeVerts = new Float32Array(nx * ny * 3)
planeOrig = new Float32Array(nx * ny * 3)
let vi = 0
for (let iy = 0; iy < ny; iy++) {
for (let ix = 0; ix < nx; ix++) {
const x = (ix / planeSegX - 0.5) * planeW
const z = (iy / planeSegY - 0.5) * planeH
planeVerts[vi] = x
planeVerts[vi + 1] = 0
planeVerts[vi + 2] = z
planeOrig[vi] = x
planeOrig[vi + 1] = 0
planeOrig[vi + 2] = z
vi += 3
}
}
// Wireframe indices — two triangles per quad
const lines = []
for (let iy = 0; iy < ny; iy++) {
for (let ix = 0; ix < nx; ix++) {
const idx = iy * nx + ix
if (ix < planeSegX) { lines.push(idx, idx + 1) }
if (iy < planeSegY) { lines.push(idx, idx + nx) }
if (ix < planeSegX && iy < planeSegY) { lines.push(idx, idx + nx + 1) }
}
}
planeIdx = new Uint16Array(lines)
}
function spawnParticle(i) {
const x = randFloat(-planeW, planeW)
const z = randFloat(-planeH, planeH)
particleData[i] = {
x, y: 0, z,
vx: randFloatSpread(0.05),
vy: randFloat(0.02, 0.05),
vz: randFloatSpread(0.05),
life: randFloat(3.0, 6.0),
maxLife: 0
}
particleData[i].maxLife = particleData[i].life
const p = i * 3
particlePos[p] = x; particlePos[p + 1] = 0; particlePos[p + 2] = z
const c = i * 4
const [r, g, b] = intToRGB(COLOR_PARTICLE)
particleColor[c + 0] = r
particleColor[c + 1] = g
particleColor[c + 2] = b
particleColor[c + 3] = 0
}
function init() {
canvas.width = Math.floor(window.innerWidth * 0.25)
canvas.height = Math.floor(window.innerHeight * 0.25)
canvas.style.width = window.innerWidth + 'px'
canvas.style.height = window.innerHeight + 'px'
canvas.style.imageRendering = 'pixelated'
const ratio = canvas.width / canvas.height
gl.viewport(0, 0, canvas.width, canvas.height)
gl.clearColor(...intToRGB(COLOR_BACKGROUND), 1)
gl.enable(gl.BLEND)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
buildPlane(ratio)
// Upload plane buffers
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.bufferData(gl.ARRAY_BUFFER, planeVerts, gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, planeOrigBuf)
gl.bufferData(gl.ARRAY_BUFFER, planeOrig, gl.STATIC_DRAW)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, planeIdx, gl.STATIC_DRAW)
// Init particles if first time
if (particleData.length === 0) {
particlePos = new Float32Array(PARTICLE_COUNT * 3)
particleColor = new Float32Array(PARTICLE_COUNT * 4)
for (let i = 0; i < PARTICLE_COUNT; i++) spawnParticle(i)
}
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.bufferData(gl.ARRAY_BUFFER, particlePos, gl.DYNAMIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.bufferData(gl.ARRAY_BUFFER, particleColor, gl.DYNAMIC_DRAW)
// Build MVP
const proj = mat4Perspective(mat4(), degToRad(70), ratio, 0.1, 100)
let view = mat4Identity(mat4())
view = mat4Translate(view, 0, -7.5, -15)
view = mat4RotateX(view, degToRad(33.75))
const mvp = mat4Multiply(mat4(), proj, view)
gl.useProgram(planeProg)
gl.uniformMatrix4fv(uMVPPlane, false, mvp)
gl.uniform3f(uFogColorPlane, ...intToRGB(COLOR_BACKGROUND))
gl.uniform1f(uFogNearPlane, 2)
gl.uniform1f(uFogFarPlane, 22)
gl.useProgram(particleProg)
gl.uniformMatrix4fv(uMVPParticle, false, mvp)
gl.uniform3f(uFogColorParticle, ...intToRGB(COLOR_BACKGROUND))
gl.uniform1f(uFogNearParticle, 6)
gl.uniform1f(uFogFarParticle, 22)
}
function updatePlane() {
for (let i = 0; i < planeVerts.length; i += 3) {
const x = planeOrig[i], z = planeOrig[i + 2]
const dist = Math.sqrt(x * x + z * z)
planeVerts[i] = x
planeVerts[i + 1] = Math.sin(dist * 0.5 - time) * 0.5 + (-Math.exp(-dist * 0.1) * 3.5)
planeVerts[i + 2] = z
}
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, planeVerts)
}
function updateParticles(delta) {
for (let i = 0; i < PARTICLE_COUNT; i++) {
const p = particleData[i]
p.life -= delta
if (p.life <= 0) { spawnParticle(i); continue }
const t = p.life / p.maxLife
p.x += p.vx * delta * 20
p.y += p.vy * delta * 20
p.z += p.vz * delta * 20
const pi = i * 3
particlePos[pi] = p.x; particlePos[pi + 1] = p.y; particlePos[pi + 2] = p.z
particleColor[i * 4 + 3] = Math.min(t * 1.2, 1)
}
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleColor)
}
function drawPlane() {
gl.useProgram(planeProg)
gl.uniform1f(uTimePlane, time)
const aPos = gl.getAttribLocation(planeProg, 'aPosition')
gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
gl.enableVertexAttribArray(aPos)
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
gl.drawElements(gl.LINES, planeIdx.length, gl.UNSIGNED_SHORT, 0)
}
function drawParticles() {
gl.useProgram(particleProg)
const aPos = gl.getAttribLocation(particleProg, 'aPosition')
const aCol = gl.getAttribLocation(particleProg, 'aColor')
gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
gl.enableVertexAttribArray(aPos)
gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)
gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
gl.enableVertexAttribArray(aCol)
gl.vertexAttribPointer(aCol, 4, gl.FLOAT, false, 0, 0)
gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT)
}
function animate(now) {
requestAnimationFrame(animate)
if (now - lastFrame < FRAME_TIME) return
const delta = (now - lastTime) * 0.00008 || 0
lastFrame = now; lastTime = now; time += delta
gl.clear(gl.COLOR_BUFFER_BIT)
updatePlane()
updateParticles(delta)
drawPlane()
drawParticles()
}
// --- Bootstrap ---
canvas = document.createElement('canvas')
gl = canvas.getContext('webgl')
planeProg = createProgram(PLANE_VERT, PLANE_FRAG)
particleProg = createProgram(PARTICLE_VERT, PARTICLE_FRAG)
planeBuf = gl.createBuffer()
planeOrigBuf = gl.createBuffer()
planeIdxBuf = gl.createBuffer()
particlePosBuf = gl.createBuffer()
particleColorBuf = gl.createBuffer()
uTimePlane = gl.getUniformLocation(planeProg, 'uTime')
uMVPPlane = gl.getUniformLocation(planeProg, 'uMVP')
uFogColorPlane = gl.getUniformLocation(planeProg, 'uFogColor')
uFogNearPlane = gl.getUniformLocation(planeProg, 'uFogNear')
uFogFarPlane = gl.getUniformLocation(planeProg, 'uFogFar')
uMVPParticle = gl.getUniformLocation(particleProg, 'uMVP')
uFogColorParticle = gl.getUniformLocation(particleProg, 'uFogColor')
uFogNearParticle = gl.getUniformLocation(particleProg, 'uFogNear')
uFogFarParticle = gl.getUniformLocation(particleProg, 'uFogFar')
if (window.outerWidth >= 1024) {
window.addEventListener('resize', init)
init()
animate(0)
} else {
init()
}
return canvas
})())
@@ -0,0 +1,344 @@
(() => {
const encodeSVG /**/ = svg => "data:image/svg+xml;base64," + btoa(svg)
const formatDate /**/ = str => new Date(str).toDateString().slice(4).toUpperCase()
const icons = {
// NOTE: External Images should have the version parameter appended to them.
// e.g. /public/.../image.png?v=${dataVersion}
diamond_fill: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon points="8,0 16,8 8,16 0,8" style="fill:#a0a0a0"/></svg>`),
diamond_empty: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8,0L0,8l8,8,8-8L8,0ZM3,8l5-5,5,5-5,5-5-5Z" style="fill: #a0a0a0"/></svg>`),
icon_cross: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><polygon style="fill:#c0c0c0" points="0 0 0 3 6 8 0 13 0 16 3 16 8 10 13 16 16 16 16 13 10 8 16 3 16 0 13 0 8 6 3 0 0 0"/></svg>`),
icon_rss: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><polygon style="fill:#c0c0c0" points="6.4 64 0 57.6 6.4 48 16 57.6 6.4 64"/><polyline style="fill:#c0c0c0" points="25.6 64 38.4 64 33.6 30.4 0 25.6 0 38.4 20.8 43.2"/><polyline style="fill:#c0c0c0" points="51.2 64 64 64 59.2 4.8 0 0 0 12.8 46.4 17.6"/></svg>`),
icon_top: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><polygon style="fill:#c0c0c0" points="1 0 31 0 32 6 0 6 1 0"/><polygon style="fill:#c0c0c0" points="0 32 0 24 16 8 32 24 32 32 16 20 0 32"/></svg>`),
tags_development: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8"><polygon style="fill:#c0c0c0" points="8 2 5 2 4 0 3 2 0 2 2 4 0 8 4 6 8 8 6 4 8 2"/></svg>`),
tags_personal: /**/ encodeSVG(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 13"><polygon style="fill:#c0c0c0" points="9 13 0 4 4 0 9 3 14 0 18 4 9 13"/></svg>`),
author_bakonpancakz: /**/ `data:image/gif;base64,R0lGODlhRABOAPAAAAAAAMDAwCH5BAUKAAAALAAAAABEAE4AAAL/hI+pF+0P45p0uQqi3hD79CjcSF6fZzLlyp1iCrKy5KphPOe3SyP6D0N1fMBioGYxKpFE5ZKZcTqR0urxZM0Ks9Utd1r5WimksFgzKWNy5FHavZ61W0m6eeXt4TYfVp9fBxhX8of2AjeoVrjTZHenmNcQKPiIGHm1RzkHmRhkQHhp2Mlo47hpeUr66TeJV6kadaaZCSurdSjVmIsLtjrG++TbBQzUamT887arWxQqR1s8qkOcLM0GXR0rWRqNbZvN/N3NLd7sXf4cjn59vg7a7o4qHJ+uTs85f+8Krz/E3+9JG0B8AgeaymdQlL2EnhgSdCivIMSAE2dJrIgMI7mKPow4OvJICaRCkXpI+rvokdpEZezSTYvyj5SMjWq20ZS3D6Ggjjkz2sy05RwVPTxgQTk6NCDSpYt+Mn2aB0EBADs=`,
}
// Parse Data from Webpage //
const dataVersion /**/ = document.querySelector("data[key='version']").value
const dataManifest /**/ = JSON.parse(document.querySelector("data[key='manifest']").value)
{
// Additional Validation //
if (!dataManifest.articles) /**/ throw "Missing Key: .articles" // Type: Metadata
if (!dataManifest.authors) /**/ throw "Missing Key: .authors" // Type: Filter
if (!dataManifest.links) /**/ throw "Missing Key: .links" // Type: Metadata
if (!dataManifest.tags) /**/ throw "Missing Key: .tags" // Type: Filter
const lookupAuthor /**/ = new Set(dataManifest.authors.map(t => t.id))
const lookupTags /**/ = new Set(dataManifest.tags.map(t => t.id))
console.info("Authors IDs:", [...lookupAuthor].join(", "))
console.info("Tags IDs: ", [...lookupTags].join(", "))
dataManifest.articles = dataManifest.articles.filter((a, i) => {
if (!a.date) {
console.warn(`Article '${a.id}' has no date and will be hidden.`)
return false
}
for (const t of a.tags) {
if (!lookupTags.has(t)) {
console.warn(`Article '${a.id}' has an unknown tag id (${t})`)
return false
}
}
if (!lookupAuthor.has(a.author)) {
console.warn(`Article '${a.id}' has an unknown author id (${a.author})`)
return false
}
console.info(`Article OK: ${a.id}`)
return true
})
}
// Render Functions //
let articleHeader /**/ = document.querySelector("div.article-header")
let articleContent /**/ = document.querySelector("div.article-content")
let navigation /**/ = document.querySelector("nav.section-navigation")
let listArticles /**/ = document.querySelector("div.article-list")
let listChapters /**/ = null
function updateNavigation() {
function templateLink(link, name, description, icon) {
const image = icon.startsWith("script:")
? icons[icon.slice(7)]
: icon // use uri
const html = `
<a class="item-nav animation-fadein" href="${link}">
<div class="item-nav-text">
<p class="animation-scrollin item-nav-text-anchor">${name}</p>
<p class="animation-scrollin item-nav-text-content">${description}</p>
</div>
<div class="item-nav-icon">
<div class="item-nav-icon-background"></div>
<img class="item-nav-icon-foreground" alt="Icon for ${name}" src="${image}">
</div>
</a>
`
return html
}
function templateList(name) {
let htmlKey /**/ = name.toLocaleLowerCase()
let dataKey /**/ = `blog_togglestate_${htmlKey}`
let result /**/ = localStorage.getItem(dataKey)
let active /**/ = (result === null) ? true : (result === "true")
// Create Toggle Buttons //
navigation.insertAdjacentHTML("beforeend", `
<button class="nav-header" for="${htmlKey}">
<span>${name}</span>
<img alt="Toggle open state for list ${name}">
</button>
<div class="nav-list" id="${htmlKey}">
</div>
`)
const list /**/ = navigation.querySelector(`div.nav-list#${htmlKey}`)
const header /**/ = navigation.querySelector(`button.nav-header[for="${htmlKey}"]`)
const icon /**/ = header.querySelector("img")
function update() {
localStorage.setItem(dataKey, active)
list.style.display = active
? "" // reset
: "none"
icon.src = active
? icons.diamond_fill
: icons.diamond_empty
active
? header.classList.add("open")
: header.classList.remove("open")
}
header.onclick = () => {
active = !active
update()
}
update()
return list
}
// Render Nav Links //
templateList("LINKS").innerHTML = dataManifest.links
.map(l => templateLink(l.id, l.name, l.description, l.icon))
.join("\n")
templateList("TAGS").innerHTML = dataManifest.tags
.map(t => templateLink(`/blog/?filter[tag]=${t.id}`, t.name, t.description, t.icon))
.join("\n")
templateList("AUTHORS").innerHTML = dataManifest.authors
.map(a => templateLink(`/blog/?filter[author]=${a.id}`, a.name, a.description, a.icon))
.join("\n")
listChapters = templateList("CHAPTERS")
}
updateNavigation()
function updateArticle() {
articleContent.replaceChildren()
articleHeader.replaceChildren()
listArticles.replaceChildren()
listChapters.replaceChildren()
const slug /**/ = new URL(window.location.href).pathname.slice("/blog/".length)
const article /**/ = dataManifest.articles.find(a => a.id == slug)
if (article) {
// Display Article //
document.title = `panca.kz - Blog - ${article.title}`
// Download Article Content [ASYNC] //
fetch(`/public/blog/${slug}/content.html?v=${dataVersion}`)
.then(async resp => {
// Insert Article Content //
// Yes, we are exploiting ourselves (safely). But I want the ability to
// implement custom elements and features to specific articles in the future.
if (!resp.ok) {
throw `Server responded with ${resp.status} ${resp.statusText}`
}
articleContent.insertAdjacentHTML("beforeend", await resp.text())
articleContent.querySelectorAll("script").forEach(s => {
eval(s.innerHTML)
})
// Generate Article Chapters //
let chapterHTML = ""
for (const item of articleContent.children) {
const normalize = c => {
const v = c.textContent.toLocaleLowerCase().replaceAll(" ", "-")
c.id = v
return v
}
if (item.classList.contains("element-header")) {
chapterHTML += `<a class="animation-scrollin chapter" href="#${normalize(item)}">${item.textContent}</a>\n`
}
if (item.classList.contains("element-subheader")) {
chapterHTML += `<a class="animation-scrollin section" href="#${normalize(item)}">${item.textContent}</a>\n`
}
}
listChapters.innerHTML = chapterHTML
})
.catch(err => {
// Generic Fetch Error //
console.error("<Cannot Download Article!>", err)
articleContent.innerHTML += `
<p style="color: red; text-align: center;" class="animation-blink">
Fetch Error, please see console for more information.
</p>
`
})
// Render Article Header (Metadata) //
const author /**/ = dataManifest.authors.find(a => a.id === article.author)
const tags /**/ = article.tags.map(id => dataManifest.tags.find(t => t.id === id))
listChapters.innerHTML += `
<p class="nav-message">... LOADING ARTICLE ...</p>
`
articleHeader.innerHTML += `
<img
id="header-banner"
fetchpriority="high"
alt="Banner for ${article.title}"
src="${article.banner_main}?v=${dataVersion}">
<h1 class="effect-docked" id="header-title">
${article.title}
</h1>
<div class="header-chevrons">
<div class="effect-chevron-container" id="header-about">
<span class="effect-chevron-point-right" id="article-author">
${author.name}
</span>
<span class="effect-chevron-point-right" id="article-published">
${formatDate(article.date)}
</span>
</div>
<div class="effect-chevron-container" id="header-tags">
${tags.map(t => `<span class="effect-chevron-point-left">${t.name}</span>`).join("\n")}
</div>
</div>
`
} else {
// Display Search //
document.title = `panca.kz - Blog`
// Collect Filters and Articles //
const params = new URLSearchParams(window.location.search)
const filterTags = new Set(params
.getAll("filter[tag]")
.filter(s => dataManifest.tags.findIndex(t => t.id.localeCompare(s) === 0) > -1)
)
const filterAuthor = new Set(params
.getAll("filter[author]")
.filter(s => dataManifest.authors.findIndex(t => t.id.localeCompare(s) === 0) > -1)
)
const results = dataManifest.articles
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.filter(a => {
// Need Requested Author //
if (filterAuthor.size > 0) {
if (!filterAuthor.has(a.author)) return false
}
// Need Requested Tags //
if (filterTags.size > 0) {
let missedTag = false
filterTags.forEach(t => {
if (a.tags.findIndex(s => s === t) === -1) {
missedTag = true
}
})
if (missedTag) {
return false
}
}
return true
})
// Render Results //
listChapters.innerHTML += `
<p class="nav-message">... SELECT ARTICLE ...</p>
`
listArticles.innerHTML += `
<div class="item-article-count">
<p class="animation-scrollin item-article-count-text">
${results.length} RESULT${results.length === 1 ? "" : "s"}
${new Array(...filterTags.values()).map(s => `&lt;TAG:${s.toUpperCase()}&gt;`).join(" ")}
${new Array(...filterAuthor.values()).map(s => `&lt;AUTHOR:${s.toUpperCase()}&gt;`).join(" ")}
<span class="animation-blink item-article-count-cursor">
_
</span>
</p>
</div>
`
listArticles.innerHTML += results.map((a, i) => `
<a class="item-article" href="/blog/${a.id}" style="animation-delay: ${i * 200}ms;">
<div class="item-article-layer">
<img
class="item-article-banner"
onload="this.classList.add('animation-fadein')"
alt="Banner for ${a.title}"
src="${a.banner_list}?v=${dataVersion}">
</div>
<div class="item-article-layer item-article-meta">
<p class="animation-scrollin item-article-meta-title">${a.title}</p>
<p class="animation-fadein item-article-meta-description">${a.description}</p>
<div class="animation-fadein item-article-meta-tags">
<span>${formatDate(a.date)}</span>
${a.tags.map(t => `<span>${t.toUpperCase()}</span>`).join("\n")}
</div>
</div>
</a>
`)
.join("\n")
}
}
updateArticle()
// History Overrides //
window.addEventListener("popstate", () => {
updateArticle()
})
document.addEventListener("click", (e) => {
const link = e.target.closest("a")
if (!link) return
const url = new URL(link.href)
// Ignore non-blog links
if (!url.pathname.startsWith("/blog/")) return
if (url.pathname.startsWith("/blog/rss.xml")) return
// Handle chapter links
const current = new URL(window.location.href)
if (
url.pathname === current.pathname &&
url.search === current.search &&
url.hash
) {
// Smooth scroll into section
e.preventDefault()
const target = document.querySelector(url.hash)
if (target) {
target.scrollIntoView({ behavior: "smooth" })
}
return
}
// Update Application
e.preventDefault()
history.pushState(null, "", url.pathname + url.search + url.hash)
updateArticle()
})
})()