-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Python Arcade
+
+
+
+ System Status: Nominal
+
+
+
+
+ Build tiny Python
+ projects & play
+
+
+
Jump into browser-ready games, math experiments, and utility labs โ each one ready in your browser, no install required.
+
+
+
+
+ 0 total projects
+ 0 games
+ 0 utilities
+
+
+
+
+
+
+
+
+
+
+
TRACK-ROUTE
+
+
+
+
+
+
01
+
Welcome
+
Jump into a tiny Python arcade built for curious beginners
+
+
+
+
+
+
+
+
+
+
+
+
02
+
Explore
+
Browse games, math puzzles & utilities โ each one ready in your browser
+
+
+
+
+
+
+
+
03
+
Play
+
Launch any project instantly, experiment with code, and share your wins
+
+
+
+
+
+
+
+
+
+
+
+
04
+
Learn
+
Pick up Python concepts as you play โ loops, logic, data & more
+
+
+
+
+
+
+
+
05
+
Build
+
Create your own mini projects and share them with the community
+
+
+
+
+
+
+
+
+
+
+
+
06
+
Share
+
Show off your work, get feedback, and keep growing as a dev
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
No projects found
-
Try adjusting your search or category filter.
-
+
-
-
-
-
+
-
-
Interactive project
-
-
-
+
+
Interactive project
+
+
+
-
+
+
-
+
+
-
+
@@ -1621,67 +673,12 @@
Stay Updated
+
-
-
-
-
-
+
diff --git a/web-app/js/hero-canvas.js b/web-app/js/hero-canvas.js
index ff44838..edc05de 100644
--- a/web-app/js/hero-canvas.js
+++ b/web-app/js/hero-canvas.js
@@ -1,297 +1,181 @@
-/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- ANIMATED SNAKES & LADDERS BOARD ON CANVAS
- Fixed: canvas now fills 100% of hero section
-โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
-const canvas = document.getElementById('boardCanvas');
-
-if (canvas) {
- const ctx = canvas.getContext('2d');
-
- /* Force canvas to fill the hero section absolutely */
-(function positionCanvas() {
- const hero = canvas.closest('.hero-section') || canvas.parentElement;
- if (hero && getComputedStyle(hero).position === 'static') {
- hero.style.position = 'relative';
- }
- canvas.style.cssText = [
- 'position:absolute',
- 'top:0', 'left:0', 'right:0', 'bottom:0',
- 'width:100%', 'height:100%',
- 'z-index:0',
- 'pointer-events:none',
- 'display:block',
- ].join(';');
-})();
-
-function resize() {
- canvas.width = canvas.offsetWidth * devicePixelRatio;
- canvas.height = canvas.offsetHeight * devicePixelRatio;
- ctx.setTransform(1, 0, 0, 1, 0, 0);
- ctx.scale(devicePixelRatio, devicePixelRatio);
-}
-resize();
-window.addEventListener('resize', resize);
-
-const W = () => canvas.offsetWidth;
-const H = () => canvas.offsetHeight;
-
-/* โโ Colour palette (pastel greens) โโ */
-const C = {
- boardLight: '#fff5e6',
- boardDark: '#f6ead7',
- gridLine: 'rgba(132,97,67,0.12)',
- snakeBody: '#f08a6a',
- snakeHead: '#d46c4f',
- ladderRail: '#d19a55',
- ladderRung: '#f0c47c',
- dice: 'rgba(255,255,255,0.88)',
- diceDot: '#6abf8d',
- token1: '#6abf8d',
- token2: '#c7a36b',
- num: 'rgba(132,97,67,0.45)',
-};
-
-/* โโ Board geometry โโ */
-const ROWS = 10, COLS = 10;
-
-function cellRect(col, row) {
- const w = W(), h = H();
- const cw = w / COLS, ch = h / ROWS;
- return { x: col * cw, y: (ROWS - 1 - row) * ch, w: cw, h: ch };
-}
-
-function cellCenter(n) {
- const idx = n - 1;
- const row = Math.floor(idx / COLS);
- let col = idx % COLS;
- if (row % 2 === 1) col = COLS - 1 - col;
- const r = cellRect(col, row);
- return { x: r.x + r.w / 2, y: r.y + r.h / 2 };
-}
-
-/* โโ Snakes & Ladders definitions โโ */
-const snakes = [[97,78],[95,56],[88,24],[76,37],[74,53],[62,19],[54,34],[19,7]];
-const ladders = [[4,25],[9,31],[20,41],[28,84],[40,59],[51,67],[63,81],[71,91]];
-
-/* โโ Animated tokens โโ */
-class Token {
- constructor(color, speed) {
- this.cell = 1 + Math.floor(Math.random() * 100);
- this.color = color;
- this.speed = speed;
- this.wait = Math.random() * 200;
- this.r = 0;
- const c = cellCenter(this.cell);
- this.x = c.x; this.y = c.y;
- this.tx = c.x; this.ty = c.y;
- }
- update() {
- if (this.wait > 0) { this.wait--; return; }
- const dx = this.tx - this.x, dy = this.ty - this.y;
- const dist = Math.sqrt(dx * dx + dy * dy);
- if (dist < 1.5) {
- this.x = this.tx; this.y = this.ty;
- if (Math.random() < 0.015) {
- this.cell = 1 + Math.floor(Math.random() * 100);
- const c = cellCenter(this.cell);
- this.tx = c.x; this.ty = c.y;
- this.wait = 30 + Math.random() * 80;
- }
- } else {
- this.x += dx * this.speed;
- this.y += dy * this.speed;
- }
- this.r += 0.04;
- }
- draw() {
- const s = Math.min(W(), H()) * 0.025;
- ctx.save();
- ctx.translate(this.x, this.y);
- ctx.rotate(Math.sin(this.r) * 0.3);
- ctx.beginPath();
- ctx.arc(0, 0, s, 0, Math.PI * 2);
- ctx.fillStyle = this.color;
- ctx.shadowColor = this.color;
- ctx.shadowBlur = 10;
- ctx.fill();
- ctx.shadowBlur = 0;
- ctx.restore();
- }
-}
-
-const tokens = [
- new Token(C.token1, 0.06),
- new Token(C.token2, 0.05),
-];
-
-/* โโ Dice animation โโ */
-const diceAnim = { val: 1, t: 0 };
-
-/* โโ Draw board โโ */
-function drawBoard() {
- const w = W(), h = H();
- ctx.fillStyle = '#fff9f0';
- ctx.fillRect(0, 0, w, h);
-
- for (let row = 0; row < ROWS; row++) {
- for (let col = 0; col < COLS; col++) {
- const r = cellRect(col, row);
- ctx.fillStyle = (row + col) % 2 === 0 ? C.boardLight : C.boardDark;
- ctx.fillRect(r.x, r.y, r.w, r.h);
- ctx.strokeStyle = C.gridLine;
- ctx.lineWidth = 0.5;
- ctx.strokeRect(r.x, r.y, r.w, r.h);
-
- /* cell numbers */
- const cellNum = (row % 2 === 0)
- ? (row * COLS + col + 1)
- : (row * COLS + (COLS - col));
- ctx.fillStyle = C.num;
- ctx.font = `bold ${Math.max(9, r.w * 0.22)}px Nunito, sans-serif`;
- ctx.textAlign = 'left';
- ctx.textBaseline = 'top';
- ctx.fillText(cellNum, r.x + r.w * 0.08, r.y + r.h * 0.06);
- }
+/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ hero-canvas.js โ Interactive Particle Mesh Background
+ Smooth, performance-first particle grid that responds to
+ mouse movement with subtle connections and gentle drift.
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+(function () {
+ 'use strict';
+
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ function ParticleMesh(canvasId) {
+ var self = this;
+ this.canvas = document.getElementById(canvasId);
+ if (!this.canvas) return;
+
+ this.ctx = this.canvas.getContext('2d');
+ this.particles = [];
+ this.mouse = { x: null, y: null, radius: 120 };
+ this.W = 0;
+ this.H = 0;
+ this.animFrame = null;
+ this.isDark = document.documentElement.getAttribute('data-theme') !== 'light';
+
+ this.CONFIG = {
+ count: 60,
+ maxDist: 140,
+ speed: 0.15,
+ connectionOpacity: this.isDark ? 0.12 : 0.08,
+ particleRadius: this.isDark ? 3.0 : 2.5,
+ color: this.isDark ? '255,255,255' : '0,0,0',
+ mousePull: 0.03,
+ };
+
+ // Particle Object constructor inside instance scope
+ function Particle() {
+ this.x = Math.random() * self.W;
+ this.y = Math.random() * self.H;
+ this.vx = (Math.random() - 0.5) * self.CONFIG.speed;
+ this.vy = (Math.random() - 0.5) * self.CONFIG.speed;
+ this.phase = Math.random() * Math.PI * 2;
}
-}
-
-/* โโ Draw snakes โโ */
-function drawSnakes() {
- snakes.forEach(([from, to]) => {
- const a = cellCenter(from), b = cellCenter(to);
- const t = performance.now() / 1200;
- const segments = 20;
- const amp = Math.min(W(), H()) * 0.025;
- ctx.beginPath();
- for (let i = 0; i <= segments; i++) {
- const p = i / segments;
- const x = a.x + (b.x - a.x) * p + Math.sin(p * Math.PI * 3 + t) * amp;
- const y = a.y + (b.y - a.y) * p;
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
+ Particle.prototype.update = function () {
+ this.phase += 0.005;
+ this.x += this.vx + Math.sin(this.phase) * 0.1;
+ this.y += this.vy + Math.cos(this.phase * 0.7) * 0.1;
+
+ /* Mouse interaction */
+ if (self.mouse.x !== null) {
+ var dx = self.mouse.x - this.x;
+ var dy = self.mouse.y - this.y;
+ var dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist < self.mouse.radius) {
+ var force = (self.mouse.radius - dist) / self.mouse.radius;
+ this.x -= dx * force * self.CONFIG.mousePull;
+ this.y -= dy * force * self.CONFIG.mousePull;
}
- ctx.strokeStyle = C.snakeBody;
- ctx.lineWidth = Math.min(W(), H()) * 0.018;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- ctx.globalAlpha = 0.7;
- ctx.stroke();
- ctx.globalAlpha = 1;
-
- /* head */
- ctx.beginPath();
- ctx.arc(a.x, a.y, Math.min(W(), H()) * 0.016, 0, Math.PI * 2);
- ctx.fillStyle = C.snakeHead;
- ctx.fill();
-
- /* eyes */
- const eyeR = Math.min(W(), H()) * 0.004;
- ctx.beginPath();
- ctx.arc(a.x - eyeR * 1.8, a.y - eyeR * 1.5, eyeR, 0, Math.PI * 2);
- ctx.arc(a.x + eyeR * 1.8, a.y - eyeR * 1.5, eyeR, 0, Math.PI * 2);
- ctx.fillStyle = '#fff';
- ctx.fill();
- });
-}
-
-/* โโ Draw ladders โโ */
-function drawLadders() {
- ladders.forEach(([from, to]) => {
- const a = cellCenter(from), b = cellCenter(to);
- const dx = b.x - a.x, dy = b.y - a.y;
- const len = Math.sqrt(dx * dx + dy * dy);
- const ux = -dy / len, uy = dx / len;
- const rail = Math.min(W(), H()) * 0.012;
-
- [rail, -rail].forEach(off => {
- ctx.beginPath();
- ctx.moveTo(a.x + ux * off, a.y + uy * off);
- ctx.lineTo(b.x + ux * off, b.y + uy * off);
- ctx.strokeStyle = C.ladderRail;
- ctx.lineWidth = Math.min(W(), H()) * 0.006;
- ctx.globalAlpha = 0.75;
- ctx.stroke();
- ctx.globalAlpha = 1;
- });
-
- const rungs = Math.round(len / (Math.min(W(), H()) * 0.065));
- for (let i = 1; i < rungs; i++) {
- const p = i / rungs;
- const rx = a.x + dx * p, ry = a.y + dy * p;
- ctx.beginPath();
- ctx.moveTo(rx + ux * rail, ry + uy * rail);
- ctx.lineTo(rx - ux * rail, ry - uy * rail);
- ctx.strokeStyle = C.ladderRung;
- ctx.lineWidth = Math.min(W(), H()) * 0.005;
- ctx.globalAlpha = 0.7;
- ctx.stroke();
- ctx.globalAlpha = 1;
+ }
+
+ /* Wrap around edges */
+ if (this.x < 0) this.x = self.W;
+ if (this.x > self.W) this.x = 0;
+ if (this.y < 0) this.y = self.H;
+ if (this.y > self.H) this.y = 0;
+ };
+
+ Particle.prototype.draw = function () {
+ self.ctx.beginPath();
+ self.ctx.arc(this.x, this.y, self.CONFIG.particleRadius, 0, Math.PI * 2);
+ self.ctx.fillStyle = 'rgba(' + self.CONFIG.color + ', 0.6)';
+ self.ctx.fill();
+ };
+
+ this.resize = function () {
+ var rect = self.canvas.parentElement.getBoundingClientRect();
+ self.W = self.canvas.width = rect.width * window.devicePixelRatio;
+ self.H = self.canvas.height = rect.height * window.devicePixelRatio;
+ self.canvas.style.width = rect.width + 'px';
+ self.canvas.style.height = rect.height + 'px';
+ self.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+ self.W = rect.width;
+ self.H = rect.height;
+ };
+
+ this.drawConnections = function () {
+ var len = self.particles.length;
+ for (var i = 0; i < len; i++) {
+ for (var j = i + 1; j < len; j++) {
+ var dx = self.particles[i].x - self.particles[j].x;
+ var dy = self.particles[i].y - self.particles[j].y;
+ var dist = dx * dx + dy * dy;
+
+ if (dist < self.CONFIG.maxDist * self.CONFIG.maxDist) {
+ var opacity = (1 - dist / (self.CONFIG.maxDist * self.CONFIG.maxDist)) * self.CONFIG.connectionOpacity;
+ self.ctx.beginPath();
+ self.ctx.moveTo(self.particles[i].x, self.particles[i].y);
+ self.ctx.lineTo(self.particles[j].x, self.particles[j].y);
+ self.ctx.strokeStyle = 'rgba(' + self.CONFIG.color + ', ' + opacity + ')';
+ self.ctx.lineWidth = 0.5;
+ self.ctx.stroke();
+ }
}
+ }
+ };
+
+ this.animate = function () {
+ self.ctx.clearRect(0, 0, self.W, self.H);
+
+ for (var i = 0; i < self.particles.length; i++) {
+ self.particles[i].update();
+ self.particles[i].draw();
+ }
+
+ self.drawConnections();
+
+ self.animFrame = requestAnimationFrame(self.animate.bind(self));
+ };
+
+ this.onMouseMove = function (e) {
+ var rect = self.canvas.getBoundingClientRect();
+ self.mouse.x = e.clientX - rect.left;
+ self.mouse.y = e.clientY - rect.top;
+ };
+
+ this.onMouseLeave = function () {
+ self.mouse.x = null;
+ self.mouse.y = null;
+ };
+
+ this.onThemeChange = function () {
+ self.isDark = document.documentElement.getAttribute('data-theme') !== 'light';
+ self.CONFIG.color = self.isDark ? '255,255,255' : '0,0,0';
+ self.CONFIG.connectionOpacity = self.isDark ? 0.12 : 0.08;
+ self.CONFIG.particleRadius = self.isDark ? 3.0 : 2.5;
+ };
+
+ this.init = function () {
+ self.resize();
+ self.particles = [];
+ for (var i = 0; i < self.CONFIG.count; i++) {
+ self.particles.push(new Particle());
+ }
+ self.animate();
+ };
+
+ this.destroy = function () {
+ if (self.animFrame) cancelAnimationFrame(self.animFrame);
+ };
+
+ // Listen to resize and hover events
+ window.addEventListener('resize', this.resize.bind(this));
+ this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
+ this.canvas.addEventListener('mouseleave', this.onMouseLeave.bind(this));
+
+ this.init();
+ }
+
+ // Initialize animations on both canvases
+ var mesh1 = new ParticleMesh('boardCanvas');
+ var mesh2 = new ParticleMesh('timelineCanvas');
+
+ /* โโ Observe theme changes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var themeObserver = new MutationObserver(function (mutations) {
+ mutations.forEach(function (m) {
+ if (m.attributeName === 'data-theme') {
+ if (mesh1 && typeof mesh1.onThemeChange === 'function') mesh1.onThemeChange();
+ if (mesh2 && typeof mesh2.onThemeChange === 'function') mesh2.onThemeChange();
+ }
});
-}
-
-/* โโ Draw animated dice โโ */
-function roundRect(cx, x, y, w, h, r) {
- cx.beginPath();
- cx.moveTo(x + r, y);
- cx.lineTo(x + w - r, y); cx.arcTo(x + w, y, x + w, y + r, r);
- cx.lineTo(x + w, y + h - r); cx.arcTo(x + w, y + h, x + w - r, y + h, r);
- cx.lineTo(x + r, y + h); cx.arcTo(x, y + h, x, y + h - r, r);
- cx.lineTo(x, y + r); cx.arcTo(x, y, x + r, y, r);
- cx.closePath();
-}
-
-function diceDots(v) {
- return {
- 1: [[0, 0]],
- 2: [[-1,-1],[1,1]],
- 3: [[-1,-1],[0,0],[1,1]],
- 4: [[-1,-1],[1,-1],[-1,1],[1,1]],
- 5: [[-1,-1],[1,-1],[0,0],[-1,1],[1,1]],
- 6: [[-1,-1],[1,-1],[-1,0],[1,0],[-1,1],[1,1]],
- }[v] || [];
-}
-
-function drawDice() {
- const w = W(), h = H();
- const size = Math.min(w, h) * 0.055;
- const px = w * 0.06, py = h * 0.12;
-
- diceAnim.t += 0.03;
- if (Math.random() < 0.01) diceAnim.val = 1 + Math.floor(Math.random() * 6);
-
- ctx.save();
- ctx.translate(px, py);
- ctx.rotate(Math.sin(diceAnim.t * 0.7) * 0.2);
-
- ctx.fillStyle = C.dice;
- ctx.shadowColor = 'rgba(30,138,88,0.3)';
- ctx.shadowBlur = 12;
- roundRect(ctx, -size / 2, -size / 2, size, size, size * 0.18);
- ctx.fill();
- ctx.shadowBlur = 0;
-
- ctx.fillStyle = C.diceDot;
- const dr = size * 0.09;
- diceDots(diceAnim.val).forEach(([ddx, ddy]) => {
- ctx.beginPath();
- ctx.arc(ddx * size * 0.3, ddy * size * 0.3, dr, 0, Math.PI * 2);
- ctx.fill();
- });
+ });
+ themeObserver.observe(document.documentElement, { attributes: true });
- ctx.restore();
-}
+ /* โโ Cleanup on page unload โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ window.addEventListener('beforeunload', function () {
+ if (mesh1) mesh1.destroy();
+ if (mesh2) mesh2.destroy();
+ themeObserver.disconnect();
+ });
-/* โโ Main loop โโ */
-function loop() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- drawBoard();
- drawLadders();
- drawSnakes();
- tokens.forEach(t => { t.update(); t.draw(); });
- drawDice();
- requestAnimationFrame(loop);
- }
-
- loop();
-}
+})();
diff --git a/web-app/js/main.js b/web-app/js/main.js
index 40c2072..f22a683 100644
--- a/web-app/js/main.js
+++ b/web-app/js/main.js
@@ -1,1251 +1,1153 @@
-/*
- main.js โ lightweight app wiring
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- CHANGES vs original (search "โโ PLAYGROUND" for every touched area):
- 1. projectsSection reference added (to hide/show vs playground)
- 2. applyCategoryFilter() guards against 'playground' category
- 3. Tab click handler detects playground tab and delegates to
- window.playgroundAPI (defined in playground.js)
- 4. moveTabFocus() skips playground-only activation; other tabs
- call deactivatePlayground automatically
- 5. randomProjectBtn is hidden while playground is active
- โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- Everything else is 100 % identical to the original.
-*/
+/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ main.js โ App wiring for Premium Python Projects Gallery
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
function prefersReducedMotion() {
- return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
-// Accessibility helper referenced by modal code
-function setMainInert(isInert) {
- var main = document.getElementById('main-content');
- if (!main) return;
- if (isInert) main.setAttribute('inert', ''); else main.removeAttribute('inert');
-}
+function safeRun(fn) { try { fn(); } catch (e) { console.error(e); } }
-function safeRun(fn) {
- try { fn(); } catch (err) { console.error(err); }
+function debounce(fn, ms) {
+ var timer;
+ return function () {
+ var args = arguments; var ctx = this;
+ clearTimeout(timer);
+ timer = setTimeout(function () { fn.apply(ctx, args); }, ms);
+ };
}
-// Debounce function for smooth search performance
-function debounce(func, delay) {
- var timeoutId;
- return function () {
- var args = arguments;
- clearTimeout(timeoutId);
- timeoutId = setTimeout(function () { func.apply(null, args); }, delay);
- };
+function syncThemeColor(theme) {
+ var meta = document.getElementById('themeColorMeta');
+ if (meta) meta.setAttribute('content', theme === 'light' ? '#f4f6f9' : '#0c0f1a');
}
-// Sync the theme-color
tag with the current theme
-function syncThemeColor(theme) {
- var meta = document.getElementById('themeColorMeta');
- if (meta) meta.setAttribute('content', theme === 'light' ? '#fffdf8' : '#201a18');
+function escapeHtml(str) {
+ var d = document.createElement('div');
+ d.textContent = str;
+ return d.innerHTML;
}
+/* โโ DOMContentLoaded โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
document.addEventListener('DOMContentLoaded', function () {
-
- // Sort project cards lexicographically (alphabetically) by title
- var projectsGrid = document.querySelector('.projects-grid');
- var rawCards = projectsGrid ? Array.from(projectsGrid.querySelectorAll('.project-card')) : [];
- if (rawCards.length > 0) {
- rawCards.sort(function (a, b) {
- var h3A = a.querySelector('h3');
- var h3B = b.querySelector('h3');
- var titleA = h3A ? h3A.textContent.trim() : '';
- var titleB = h3B ? h3B.textContent.trim() : '';
- return titleA.localeCompare(titleB);
- });
- var reorderedCards = document.createDocumentFragment();
- rawCards.forEach(function (card) {
- reorderedCards.appendChild(card);
- });
- projectsGrid.appendChild(reorderedCards);
- }
-
- // โโ DOM references โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- var html = document.documentElement;
- var themeToggle = document.getElementById('themeToggle');
- var soundToggle = document.getElementById('soundToggle');
- var backToTopButton = document.getElementById('backToTop');
- var tabs = document.querySelectorAll('.tab');
- var projectCards = Array.from(document.querySelectorAll('.project-card'));
- var searchInput = document.getElementById('projectSearch') || document.getElementById('searchInput');
- var searchClear = document.getElementById('searchClear');
- var searchDropdown = document.getElementById('searchDropdown');
- var searchShortcut = document.getElementById('searchShortcut');
- var searchLoader = document.getElementById('searchLoader');
- var projectsSection = document.getElementById('projectsSection');
- var playgroundSection = document.getElementById('playgroundSection');
- var stickyFilterBar = document.getElementById('stickyFilterBar');
- var stickyTabs = document.querySelectorAll('.sticky-tab');
- var heroSection = document.querySelector('.hero-section');
- var cursorGlow = document.getElementById('cursorGlow');
- var heroTypewriter = document.getElementById('heroTypewriter');
- var heroProjectCount = document.getElementById('heroProjectCount');
- var heroGameCount = document.getElementById('heroGameCount');
- var heroUtilityCount = document.getElementById('heroUtilityCount');
- var revealItems = document.querySelectorAll('.reveal-on-scroll');
- var featureLaunchers = document.querySelectorAll('[data-project-target]');
- var emptyState = document.getElementById('emptyState');
- var resultsList = document.getElementById('resultsList');
- var resultsSection = document.getElementById('resultsSection');
- var recentSearchesList = document.getElementById('recentSearchesList');
- var recentSearchesSection = document.getElementById('recentSearchesSection');
- var tipsSection = document.getElementById('tipsSection');
- var noResultsMessage = document.getElementById('noResultsMessage');
- var modal = document.getElementById('projectModal');
- var modalBody = document.getElementById('modalBody');
- var modalClose = document.getElementById('modalClose');
- var modalTitle = document.getElementById('modalDialogTitle');
- /* randomProjectBtn and playgroundHeroBtn removed โ sidebar handles those actions */
-
-
- var currentCategory = 'all';
- var currentSearchQuery = '';
- var playgroundActive = false;
- var selectedSuggestionIndex = -1;
- var removeTrap = null;
- var lastFocusedElement = null;
- var recentSearches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
- //-----------------------project count badge------------------------------------
- const projectCountBadge = document.getElementById("projectCountBadge");
- const projectCount = document.querySelectorAll(".project-card").length;
- const gameCount = projectCards.filter(function (card) { return card.getAttribute('data-category') === 'games'; }).length;
- const utilityCount = projectCards.filter(function (card) { return card.getAttribute('data-category') === 'utilities'; }).length;
-
- if (projectCountBadge) {
- projectCountBadge.textContent = `${projectCount} projects`;
- }
- if (heroProjectCount) heroProjectCount.textContent = String(projectCount);
- if (heroGameCount) heroGameCount.textContent = String(gameCount);
- if (heroUtilityCount) heroUtilityCount.textContent = String(utilityCount);
-
- var rotatePhrases = [
- 'typing speed drills',
- 'math quiz rounds',
- 'logic puzzle practice',
- 'browser-ready Python wins'
- ];
-
- if (heroTypewriter && !prefersReducedMotion()) {
- var phraseIndex = 0;
- setInterval(function () {
- phraseIndex = (phraseIndex + 1) % rotatePhrases.length;
- heroTypewriter.textContent = rotatePhrases[phraseIndex];
- }, 2200);
- }
-
- if (cursorGlow && !prefersReducedMotion()) {
- document.addEventListener('pointermove', function (event) {
- cursorGlow.style.left = event.clientX + 'px';
- cursorGlow.style.top = event.clientY + 'px';
- });
- document.addEventListener('pointerleave', function () {
- cursorGlow.style.opacity = '0';
- });
- document.addEventListener('pointerenter', function () {
- cursorGlow.style.opacity = '0.5';
- });
- }
- // โโ Theme Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- function updateThemeToggleAria(isLightTheme) {
- if (!themeToggle) return;
- themeToggle.setAttribute(
- 'aria-label',
- isLightTheme ? 'Switch to dark mode' : 'Switch to light mode'
- );
- }
-
- if (themeToggle) {
- var savedTheme = localStorage.getItem('theme') || 'light';
- html.setAttribute('data-theme', savedTheme);
- syncThemeColor(savedTheme);
- themeToggle.innerHTML =
- savedTheme === 'light'
- ? '

'
- : '

';
- updateThemeToggleAria(savedTheme === 'light');
-
- themeToggle.addEventListener('click', function () {
- var currentTheme = html.getAttribute('data-theme');
- var newTheme = currentTheme === 'light' ? 'dark' : 'light';
-
- html.setAttribute('data-theme', newTheme);
- localStorage.setItem('theme', newTheme);
- syncThemeColor(newTheme);
-
- themeToggle.innerHTML =
- newTheme === 'light'
- ? '

'
- : '

';
- updateThemeToggleAria(newTheme === 'light');
- });
+ var html = document.documentElement;
+ var themeToggle = document.getElementById('themeToggle');
+ var soundToggle = document.getElementById('soundToggle');
+ var backToTopButton = document.getElementById('backToTop');
+ var searchInput = document.getElementById('searchInput');
+ var searchDropdown = document.getElementById('searchDropdown');
+ var searchLoader = document.getElementById('searchLoader');
+ var recentSearchesList = document.getElementById('recentSearchesList');
+ var recentSearchesSection = document.getElementById('recentSearchesSection');
+ var resultsList = document.getElementById('resultsList');
+ var resultsSection = document.getElementById('resultsSection');
+ var tipsSection = document.getElementById('tipsSection');
+ var noResultsMessage = document.getElementById('noResultsMessage');
+ var projectsSection = document.getElementById('projectsSection');
+ var playgroundSection = document.getElementById('playgroundSection');
+ var stickyFilterBar = document.getElementById('stickyFilterBar');
+ var stickyTabs = document.querySelectorAll('.sticky-tab');
+ var heroSection = document.querySelector('.hero-section');
+ var cursorGlow = document.getElementById('cursorGlow');
+ var heroProjectCount = document.getElementById('heroProjectCount');
+ var heroGameCount = document.getElementById('heroGameCount');
+ var heroUtilityCount = document.getElementById('heroUtilityCount');
+ var modal = document.getElementById('projectModal');
+ var modalBody = document.getElementById('modalBody');
+ var modalClose = document.getElementById('modalClose');
+ var modalTitle = document.getElementById('modalDialogTitle');
+ var exploreBtn = document.getElementById('exploreBtn');
+ var randomProjectBtn = document.getElementById('randomProjectBtn');
+ var randomProjectBtnSidebar = document.getElementById('randomProjectBtnSidebar');
+ var emptyState = document.getElementById('emptyState');
+ var emptyStateHint = document.getElementById('emptyStateHint');
+ var projectCountBadge = document.getElementById('projectCountBadge');
+ var mobileMenuToggle = document.getElementById('mobileMenuToggle');
+ var navControls = document.getElementById('navControls');
+ var navbar = document.getElementById('mainNavbar');
+
+ var currentCategory = 'all';
+ var currentSearchQuery = '';
+ var playgroundActive = false;
+ var selectedSuggestionIndex = -1;
+ var removeTrap = null;
+ var lastFocusedElement = null;
+ var projectCards = [];
+ var recentSearches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
+
+ /* โโ Helper: setMainInert โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function setMainInert(isInert) {
+ var main = document.getElementById('main-content');
+ if (!main) return;
+ if (isInert) main.setAttribute('inert', ''); else main.removeAttribute('inert');
+ }
+
+ /* โโ Theme Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function updateThemeToggleAria(isLight) {
+ if (!themeToggle) return;
+ themeToggle.setAttribute('aria-label', isLight ? 'Switch to dark mode' : 'Switch to light mode');
+ }
+
+ if (themeToggle) {
+ var savedTheme = localStorage.getItem('theme') || 'dark';
+ html.setAttribute('data-theme', savedTheme);
+ syncThemeColor(savedTheme);
+ themeToggle.innerHTML = savedTheme === 'light'
+ ? '
'
+ : '
';
+ updateThemeToggleAria(savedTheme === 'light');
+
+ themeToggle.addEventListener('click', function () {
+ var current = html.getAttribute('data-theme');
+ var next = current === 'light' ? 'dark' : 'light';
+ html.setAttribute('data-theme', next);
+ localStorage.setItem('theme', next);
+ syncThemeColor(next);
+ themeToggle.innerHTML = next === 'light'
+ ? '
'
+ : '
';
+ updateThemeToggleAria(next === 'light');
+ });
+ }
+
+ /* โโ Sound Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ if (soundToggle && window.audioController) {
+ function updateSoundIcon() {
+ soundToggle.innerHTML = window.audioController.isMuted
+ ? '
'
+ : '
';
}
-
- // โโ Sound Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- if (soundToggle) {
- var updateSoundIcon = function () {
- if (window.audioController) {
- soundToggle.innerHTML = window.audioController.isMuted
- ? '
'
- : '
';
- }
- };
+ updateSoundIcon();
+ soundToggle.addEventListener('click', function () {
+ if (typeof window.audioController.toggleMute === 'function') {
+ window.audioController.toggleMute();
updateSoundIcon();
- soundToggle.addEventListener('click', function () {
- if (window.audioController && typeof window.audioController.toggleMute === 'function') {
- window.audioController.toggleMute();
- updateSoundIcon();
- if (!window.audioController.isMuted && typeof window.audioController.play === 'function') {
- window.audioController.play('click');
- }
- }
- });
- }
-
- // โโ Back to Top โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- if (backToTopButton) {
- var toggleBackToTopButton = function () {
- backToTopButton.classList.toggle('visible', window.scrollY > 300);
- var navbar = document.querySelector('.navbar');
- if (navbar) navbar.classList.toggle('scrolled', window.scrollY > 12);
- };
- window.addEventListener('scroll', toggleBackToTopButton, { passive: true });
- toggleBackToTopButton();
-
- backToTopButton.addEventListener('click', function () {
- window.scrollTo({ top: 0, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
- });
- }
-
- // โโ PLAYGROUND: helpers to show / hide sections โโโโโโโโโโโโโโโโโ
- /* โ PLAYGROUND ADD (entire block) */
- function showProjectsSection() {
- playgroundActive = false;
- if (playgroundSection) playgroundSection.style.display = 'none';
- if (projectsSection) projectsSection.style.display = '';
- if (window.playgroundAPI && typeof window.playgroundAPI.deactivate === 'function') {
- window.playgroundAPI.deactivate();
+ if (!window.audioController.isMuted && typeof window.audioController.play === 'function') {
+ window.audioController.play('click');
}
- }
-
- function showPlaygroundSection() {
- playgroundActive = true;
- syncStickyTabs('playground');
- if (projectsSection) projectsSection.style.display = 'none';
- if (window.playgroundAPI && typeof window.playgroundAPI.activate === 'function') {
- window.playgroundAPI.activate();
- }
- }
- /* โ PLAYGROUND ADD end */
- // โโ Sticky Filter Bar: position + show/hide on scroll โโโโโโโโโโโโ
-function syncStickyTabs(category) {
- stickyTabs.forEach(function (st) {
- var selected = st.getAttribute('data-sticky-category') === category;
- st.classList.toggle('active', selected);
- st.setAttribute('aria-selected', selected ? 'true' : 'false');
- st.setAttribute('tabindex', selected ? '0' : '-1');
+ }
});
-}
-
-if (stickyFilterBar && heroSection) {
- // Position the bar directly below the navbar
- var navbar = document.querySelector('.navbar');
- function positionStickyBar() {
- var navHeight = navbar ? navbar.getBoundingClientRect().height : 0;
- stickyFilterBar.style.top = navHeight + 'px';
- }
- positionStickyBar();
- window.addEventListener('resize', positionStickyBar);
-
- var heroObserver = new IntersectionObserver(function (entries) {
- entries.forEach(function (entry) {
- stickyFilterBar.classList.toggle('visible', !entry.isIntersecting);
- });
- }, { threshold: 0 });
- heroObserver.observe(heroSection);
-}
-
- // Wire sticky tab clicks โ mirrors main tab behaviour
- stickyTabs.forEach(function (st) {
- st.addEventListener('click', function () {
- var category = st.getAttribute('data-sticky-category');
-
- // Sync sticky tabs UI
- syncStickyTabs(category);
-
- // Sync hero tabs UI
- tabs.forEach(function (t) {
- var selected = t.getAttribute('data-category') === category;
- t.classList.toggle('active', selected);
- t.setAttribute('aria-selected', selected ? 'true' : 'false');
- t.setAttribute('tabindex', selected ? '0' : '-1');
- });
-
- // Delegate section logic (same as hero tab click)
- if (category === 'playground') {
- showPlaygroundSection();
- } else {
- showProjectsSection();
- applyCategoryFilter(category);
- }
- });
+ } else if (soundToggle) {
+ soundToggle.addEventListener('click', function () {
+ var icon = soundToggle.querySelector('i');
+ if (icon) icon.className = icon.className === 'fas fa-volume-up' ? 'fas fa-volume-mute' : 'fas fa-volume-up';
});
-
- // โโ Category Filtering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- function applyCategoryFilter(category) {
- /* โโ PLAYGROUND ADD: skip filtering when playground tab is selected โโ */
- if (category === 'playground') return;
- /* โโ PLAYGROUND ADD end โโ */
-
- currentCategory = category;
- syncStickyTabs(category);
- var visibleCount = 0;
- var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
- projectCards.forEach(function (card) {
- var cardCategory = card.getAttribute('data-category');
- var projectName = card.getAttribute('data-project');
- var isFavorite = favorites.includes(projectName);
-
- if (category === 'all' ||
- (category === 'favorites' && isFavorite) ||
- (category !== 'favorites' && cardCategory === category)) {
- card.style.display = '';
- card.style.animation = prefersReducedMotion() ? 'none' : 'fadeIn 0.6s ease';
- visibleCount++;
- } else {
- card.style.display = 'none';
- }
- });
-
- if (emptyState) {
- emptyState.style.display = visibleCount === 0 ? 'block' : 'none';
- }
- if (sidebarBadge) {
- sidebarBadge.textContent = String(visibleCount);
- }
+ }
+
+ /* โโ Hero Controls Mirror Toggles โโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var heroSoundToggle = document.getElementById('heroSoundToggle');
+ var heroThemeToggle = document.getElementById('heroThemeToggle');
+
+ function syncHeroControlsIcons() {
+ if (heroSoundToggle && soundToggle) {
+ var realSoundIcon = soundToggle.querySelector('i');
+ var heroSoundIcon = heroSoundToggle.querySelector('i');
+ if (realSoundIcon && heroSoundIcon) {
+ heroSoundIcon.className = realSoundIcon.className;
+ }
}
-
- function moveTabFocus(fromIndex, delta) {
- var len = tabs.length;
- var next = (fromIndex + delta + len) % len;
- tabs.forEach(function (t, i) {
- var selected = i === next;
- t.classList.toggle('active', selected);
- t.setAttribute('aria-selected', selected ? 'true' : 'false');
- t.setAttribute('tabindex', selected ? '0' : '-1');
- });
- tabs[next].focus();
-
- /* โโ PLAYGROUND ADD: delegate to section helpers โโ */
- var nextCategory = tabs[next].getAttribute('data-category');
- if (nextCategory === 'playground') {
- showPlaygroundSection();
- } else {
- showProjectsSection();
- applyCategoryFilter(nextCategory);
- }
- /* โโ PLAYGROUND ADD end โโ */
+ if (heroThemeToggle && themeToggle) {
+ var realThemeIcon = themeToggle.querySelector('i');
+ var heroThemeIcon = heroThemeToggle.querySelector('i');
+ if (realThemeIcon && heroThemeIcon) {
+ heroThemeIcon.className = realThemeIcon.className;
+ }
}
+ }
- tabs.forEach(function (tab, index) {
- tab.addEventListener('click', function () {
- /* Update active state on all tabs */
- tabs.forEach(function (t) {
- var selected = t === tab;
- t.classList.toggle('active', selected);
- t.setAttribute('aria-selected', selected ? 'true' : 'false');
- t.setAttribute('tabindex', selected ? '0' : '-1');
- });
-
- var category = tab.getAttribute('data-category');
-
- /* โโ PLAYGROUND ADD: playground tab gets its own section โโ */
- if (category === 'playground') {
- showPlaygroundSection();
- } else {
- showProjectsSection();
- applyCategoryFilter(category);
- }
- /* โโ PLAYGROUND ADD end โโ */
- });
-
- tab.addEventListener('keydown', function (e) {
- var handled = false;
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
- moveTabFocus(index, 1);
- handled = true;
- } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
- moveTabFocus(index, -1);
- handled = true;
- } else if (e.key === 'Home') {
- moveTabFocus(index, -index);
- handled = true;
- } else if (e.key === 'End') {
- moveTabFocus(index, tabs.length - 1 - index);
- handled = true;
- }
- if (handled) e.preventDefault();
- });
+ if (heroSoundToggle && soundToggle) {
+ heroSoundToggle.addEventListener('click', function () {
+ soundToggle.click();
+ setTimeout(syncHeroControlsIcons, 50);
});
+ }
- // โโ Sidebar Filter Tabs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- var sidebarTabs = document.querySelectorAll('.sidebar-tab');
- var sidebarBadge = document.getElementById('sidebarProjectCount');
- var sidebarRandomBtn = document.getElementById('randomProjectBtnSidebar');
-
- function syncSidebarTabs(category) {
- sidebarTabs.forEach(function (st) {
- var selected = st.getAttribute('data-category') === category;
- st.classList.toggle('active', selected);
- st.setAttribute('aria-selected', selected ? 'true' : 'false');
- st.setAttribute('tabindex', selected ? '0' : '-1');
- });
- if (sidebarBadge) {
- var visible = projectCards.filter(function (c) { return c.style.display !== 'none'; }).length;
- sidebarBadge.textContent = String(visible);
- }
- }
-
- sidebarTabs.forEach(function (st) {
- st.addEventListener('click', function () {
- var category = st.getAttribute('data-category');
- syncSidebarTabs(category);
- syncStickyTabs(category);
-
- if (category === 'playground') {
- showPlaygroundSection();
- var pg = document.getElementById('playgroundSection');
- if (pg) pg.scrollIntoView({ behavior: 'smooth', block: 'start' });
- } else {
- showProjectsSection();
- applyCategoryFilter(category);
- if (projectsSection) {
- projectsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- }
- });
+ if (heroThemeToggle && themeToggle) {
+ heroThemeToggle.addEventListener('click', function () {
+ themeToggle.click();
+ setTimeout(syncHeroControlsIcons, 50);
+ });
+ }
+
+ // Initial sync on load
+ setTimeout(syncHeroControlsIcons, 100);
+
+ /* โโ Mobile Sidebar Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var mobileSidebarToggle = document.getElementById('mobileSidebarToggle');
+ var mainSidebar = document.getElementById('mainSidebar');
+ if (mobileSidebarToggle && mainSidebar) {
+ mobileSidebarToggle.addEventListener('click', function () {
+ var active = mainSidebar.classList.toggle('open');
+ mobileSidebarToggle.setAttribute('aria-expanded', active);
+ var icon = mobileSidebarToggle.querySelector('i');
+ if (icon) icon.className = active ? 'fas fa-times' : 'fas fa-bars';
});
- if (sidebarRandomBtn) {
- sidebarRandomBtn.addEventListener('click', function () {
- var visible = projectCards.filter(function (c) { return c.style.display !== 'none'; });
- var pool = visible.length ? visible : projectCards;
- var pick = pool[Math.floor(Math.random() * pool.length)];
- var name = pick.getAttribute('data-project');
- if (name && typeof openProjectSafe === 'function') {
- openProjectSafe(name, sidebarRandomBtn);
- setTimeout(function () {
- pick.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }, 300);
- }
- });
- }
-
- // โโ Search / Autocomplete โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- function getMatchingProjects(query) {
- if (!query) return [];
- var matches = [];
- projectCards.forEach(function (card) {
- var category = card.getAttribute('data-category');
- var title = card.querySelector('h3').textContent.toLowerCase();
- var description = card.querySelector('p').textContent.toLowerCase();
- var tags = (card.getAttribute('data-tags') || '').toLowerCase();
-
- var categoryMatch = currentCategory === 'all' || category === currentCategory;
- var searchMatch = title.includes(query) ||
- description.includes(query) ||
- tags.includes(query);
-
- if (categoryMatch && searchMatch) {
- matches.push({
- card: card,
- title: card.querySelector('h3').textContent,
- tags: card.getAttribute('data-tags') || '',
- category: category
- });
- }
- });
- return matches;
- }
+ document.addEventListener('click', function (e) {
+ if (mainSidebar && mobileSidebarToggle &&
+ !mainSidebar.contains(e.target) && e.target !== mobileSidebarToggle &&
+ mainSidebar.classList.contains('open')) {
+ mainSidebar.classList.remove('open');
+ mobileSidebarToggle.setAttribute('aria-expanded', 'false');
+ var icon = mobileSidebarToggle.querySelector('i');
+ if (icon) icon.className = 'fas fa-bars';
+ }
+ });
+ }
- function highlightText(container, text, query) {
- var safeQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- var chunks = text.split(new RegExp('(' + safeQuery + ')', 'gi'));
-
- chunks.forEach(function (part) {
- if (part.toLowerCase() === query.toLowerCase()) {
- var highlight = document.createElement('mark');
- highlight.style.background = 'rgba(99, 102, 241, 0.3)';
- highlight.style.color = 'var(--primary-color)';
- highlight.style.fontWeight = '600';
- highlight.textContent = part;
- container.appendChild(highlight);
- } else if (part) {
- container.appendChild(document.createTextNode(part));
- }
- });
- }
+ if (backToTopButton) {
+ var toggleBackToTop = function () {
+ backToTopButton.classList.toggle('visible', window.scrollY > 300);
+ };
+ window.addEventListener('scroll', toggleBackToTop, { passive: true });
+ toggleBackToTop();
- function updateSuggestionHighlight() {
- if (!resultsList) return;
- var items = resultsList.querySelectorAll('.dropdown-item');
- items.forEach(function (item, i) {
- item.classList.toggle('selected', i === selectedSuggestionIndex);
- });
+ backToTopButton.addEventListener('click', function () {
+ window.scrollTo({ top: 0, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
+ });
+ }
+
+ /* โโ Cursor Glow โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ if (cursorGlow && !prefersReducedMotion() && html.getAttribute('data-theme') !== 'light') {
+ var glowTimeout;
+ document.addEventListener('pointermove', function (e) {
+ cursorGlow.style.left = e.clientX + 'px';
+ cursorGlow.style.top = e.clientY + 'px';
+ cursorGlow.style.opacity = '0.5';
+ clearTimeout(glowTimeout);
+ glowTimeout = setTimeout(function () { cursorGlow.style.opacity = '0'; }, 3000);
+ });
+ document.addEventListener('pointerleave', function () {
+ cursorGlow.style.opacity = '0';
+ });
+ }
+
+ /* โโ Gather Project Cards โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var projectsGrid = document.querySelector('.projects-grid');
+ projectCards = Array.from(document.querySelectorAll('.project-card'));
+
+ /* โโ Hero Stats โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var totalCount = projectCards.length;
+ var gameCount = projectCards.filter(function (c) { return c.getAttribute('data-category') === 'games'; }).length;
+ var utilityCount = projectCards.filter(function (c) { return c.getAttribute('data-category') === 'utilities'; }).length;
+
+ if (heroProjectCount) heroProjectCount.textContent = String(totalCount);
+ if (heroGameCount) heroGameCount.textContent = String(gameCount);
+ if (heroUtilityCount) heroUtilityCount.textContent = String(utilityCount);
+ if (projectCountBadge) projectCountBadge.textContent = String(totalCount) + ' projects';
+
+ /* โโ Explore Button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ if (exploreBtn) {
+ exploreBtn.addEventListener('click', function () {
+ if (projectsSection) projectsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ });
+ }
+
+ /* โโ Category Filtering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var sidebarTabs = document.querySelectorAll('.sidebar-tab');
+ var sidebarBadge = null;
+
+ function applyCategoryFilter(category) {
+ if (category === 'playground') return;
+ currentCategory = category;
+ syncSidebarTabs(category);
+ syncStickyTabs(category);
+ var visibleCount = 0;
+ var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
+ projectCards.forEach(function (card) {
+ var cardCat = card.getAttribute('data-category');
+ var projectName = card.getAttribute('data-project');
+ var isFav = favorites.includes(projectName);
+ if (category === 'all' ||
+ (category === 'favorites' && isFav) ||
+ (category !== 'favorites' && cardCat === category)) {
+ card.style.display = '';
+ visibleCount++;
+ } else {
+ card.style.display = 'none';
+ }
+ });
+ if (emptyState) {
+ emptyState.style.display = visibleCount === 0 ? 'block' : 'none';
}
-
- function selectSuggestion(title) {
- if (!searchInput) return;
- searchInput.value = title;
- currentSearchQuery = title.toLowerCase();
- performSearch();
- closeDropdown();
- if (projectsSection) {
- projectsSection.scrollIntoView({
- behavior: prefersReducedMotion() ? 'auto' : 'smooth',
- block: 'start'
- });
- }
+ if (projectCountBadge) {
+ projectCountBadge.textContent = String(visibleCount) + ' projects';
}
+ }
- function cleanSearchHighlights() {
- projectCards.forEach(function (card) {
- var titleEl = card.querySelector('h3');
- var descEl = card.querySelector('p');
- [titleEl, descEl].forEach(function (el) {
- if (!el) return;
- el.querySelectorAll('mark.search-highlight').forEach(function (m) {
- m.replaceWith(m.textContent);
- });
- });
- });
- }
+ function syncSidebarTabs(category) {
+ sidebarTabs.forEach(function (st) {
+ var selected = st.getAttribute('data-category') === category;
+ st.classList.toggle('active', selected);
+ st.setAttribute('aria-selected', selected ? 'true' : 'false');
+ st.setAttribute('tabindex', selected ? '0' : '-1');
+ });
+ }
- function escapeHtml(str) {
- var div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
+ function syncStickyTabs(category) {
+ stickyTabs.forEach(function (st) {
+ var selected = st.getAttribute('data-sticky-category') === category;
+ st.classList.toggle('active', selected);
+ st.setAttribute('aria-selected', selected ? 'true' : 'false');
+ st.setAttribute('tabindex', selected ? '0' : '-1');
+ });
+ }
+
+ /* โโ Playground Section Toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function showProjectsSection() {
+ playgroundActive = false;
+ if (playgroundSection) playgroundSection.style.display = 'none';
+ if (projectsSection) projectsSection.style.display = '';
+ if (window.playgroundAPI && typeof window.playgroundAPI.deactivate === 'function') {
+ window.playgroundAPI.deactivate();
}
-
- function highlightCardText(el, query) {
- var safe = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- var regex = new RegExp('(' + safe + ')', 'gi');
- var escaped = escapeHtml(el.textContent);
- var html = escaped.replace(regex, '
$1');
- el.innerHTML = html;
+ }
+
+ function showPlaygroundSection() {
+ playgroundActive = true;
+ syncStickyTabs('playground');
+ if (projectsSection) projectsSection.style.display = 'none';
+ if (playgroundSection) {
+ playgroundSection.style.display = '';
+ if (window.playgroundAPI && typeof window.playgroundAPI.activate === 'function') {
+ window.playgroundAPI.activate();
+ }
}
-
- function performSearch() {
- var query = currentSearchQuery;
- cleanSearchHighlights();
-
- if (!query) {
- applyCategoryFilter(currentCategory);
- updateSearchResultCount(0);
- var hint = document.getElementById('emptyStateHint');
- if (hint) hint.textContent = 'Try adjusting your search or category filter.';
- return;
- }
-
- if (currentCategory !== 'all') {
- currentCategory = 'all';
- syncSidebarTabs('all');
- syncStickyTabs('all');
- }
-
- recentSearches = recentSearches.filter(function (s) { return s !== query; });
- recentSearches.unshift(query);
- recentSearches = recentSearches.slice(0, 10);
- localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
-
- var visibleCount = 0;
- var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
- projectCards.forEach(function (card) {
- var category = card.getAttribute('data-category');
- var titleText = (card.querySelector('h3') || {}).textContent || '';
- var descText = (card.querySelector('p') || {}).textContent || '';
- var title = titleText.toLowerCase();
- var description = descText.toLowerCase();
- var tags = (card.getAttribute('data-tags') || '').toLowerCase();
- var projectName = card.getAttribute('data-project');
- var isFavorite = favorites.includes(projectName);
-
- var categoryMatch = currentCategory === 'all' ||
- (currentCategory === 'favorites' && isFavorite) ||
- (currentCategory !== 'favorites' && category === currentCategory);
- var searchMatch = title.includes(query) ||
- description.includes(query) ||
- tags.includes(query);
-
- if (categoryMatch && searchMatch) {
- card.style.display = '';
- card.style.animation = prefersReducedMotion() ? 'none' : 'fadeIn 0.5s ease';
- visibleCount++;
- var titleEl = card.querySelector('h3');
- var descEl = card.querySelector('p');
- if (titleEl) highlightCardText(titleEl, query);
- if (descEl) highlightCardText(descEl, query);
- } else {
- card.style.display = 'none';
- }
- });
-
- if (emptyState) {
- var showEmpty = visibleCount === 0;
- emptyState.style.display = showEmpty ? 'block' : 'none';
- if (showEmpty) {
- var hint = document.getElementById('emptyStateHint');
- if (hint) hint.textContent = 'No projects match "' + query + '". Try a different keyword.';
- }
+ }
+
+ /* โโ Sidebar Tabs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ sidebarTabs.forEach(function (st) {
+ st.addEventListener('click', function () {
+ var category = st.getAttribute('data-category');
+
+ var pageCategory = document.body.getAttribute('data-page');
+ if (pageCategory) {
+ // We are on a subpage (games, math, or utilities)
+ if (category === pageCategory) {
+ var grid = document.getElementById('projectsGrid');
+ if (grid) grid.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ return;
}
+ var pageMap = {
+ 'all': 'index.html',
+ 'games': 'games.html',
+ 'math': 'math.html',
+ 'utilities': 'utilities.html',
+ 'favorites': 'index.html?category=favorites',
+ 'playground': 'index.html?category=playground'
+ };
+ window.location.href = pageMap[category] || 'index.html';
+ return;
+ }
+
+ syncSidebarTabs(category);
+ syncStickyTabs(category);
+
+ if (category === 'playground') {
+ showPlaygroundSection();
+ if (playgroundSection) playgroundSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ } else {
+ showProjectsSection();
+ applyCategoryFilter(category);
+ if (projectsSection) projectsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+ });
+
+ /* โโ Sticky Tabs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ stickyTabs.forEach(function (st) {
+ st.addEventListener('click', function () {
+ var category = st.getAttribute('data-sticky-category');
+ syncStickyTabs(category);
+ syncSidebarTabs(category);
+
+ if (category === 'playground') {
+ showPlaygroundSection();
+ if (playgroundSection) playgroundSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ } else {
+ showProjectsSection();
+ applyCategoryFilter(category);
+ if (projectsSection) projectsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+ });
+ });
- updateSearchResultCount(visibleCount);
- }
+ /* โโ Sticky Filter Bar Visibility โโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ if (stickyFilterBar && heroSection) {
+ var heroObserver = new IntersectionObserver(function (entries) {
+ entries.forEach(function (entry) {
+ stickyFilterBar.classList.toggle('visible', !entry.isIntersecting);
+ });
+ }, { threshold: 0, rootMargin: '-80px 0px 0px 0px' });
+ heroObserver.observe(heroSection);
- function updateSearchResultCount(count) {
- var badge = document.getElementById('searchResultCount');
- if (!badge) {
- if (!searchInput) return;
- badge = document.createElement('span');
- badge.id = 'searchResultCount';
- badge.style.cssText = 'font-family:IBM Plex Mono,monospace;font-size:0.72rem;color:var(--text-secondary);margin-left:0.5rem;white-space:nowrap;opacity:0;transition:opacity 0.2s ease';
- searchInput.parentNode.appendChild(badge);
- }
- if (count > 0) {
- badge.textContent = count + ' result' + (count !== 1 ? 's' : '');
- badge.style.opacity = '1';
- } else {
- badge.style.opacity = '0';
- }
+ window.addEventListener('scroll', function () {
+ var navH = navbar ? navbar.getBoundingClientRect().height : 72;
+ stickyFilterBar.style.top = (navH + 16) + 'px';
+ }, { passive: true });
+ }
+
+ /* โโ Random Project โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function openRandomProject(trigger) {
+ var visible = projectCards.filter(function (c) { return c.style.display !== 'none'; });
+ var pool = visible.length ? visible : projectCards;
+ var pick = pool[Math.floor(Math.random() * pool.length)];
+ var name = pick.getAttribute('data-project');
+ if (name && typeof openProjectSafe === 'function') {
+ openProjectSafe(name, trigger);
}
-
- function closeDropdown() {
- if (searchDropdown) searchDropdown.classList.remove('active');
+ }
+
+ if (randomProjectBtn) {
+ randomProjectBtn.addEventListener('click', function () { openRandomProject(randomProjectBtn); });
+ }
+ if (randomProjectBtnSidebar) {
+ randomProjectBtnSidebar.addEventListener('click', function () { openRandomProject(randomProjectBtnSidebar); });
+ }
+
+ /* โโ Init sidebar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var pageCategory = document.body.getAttribute('data-page');
+ if (sidebarTabs.length) {
+ if (pageCategory) {
+ syncSidebarTabs(pageCategory);
+ } else {
+ syncSidebarTabs('all');
}
+ }
+ if (stickyTabs.length) syncStickyTabs('all');
+
+ /* โโ Sidebar Active Scroll Observer โโโโโโโโโโโโโโโโโโโโโโโโโ */
+ if (!pageCategory && projectsSection) {
+ // On homepage, observe projectsSection to toggle sidebar-active class
+ var sidebarActiveObserver = new IntersectionObserver(function (entries) {
+ entries.forEach(function (entry) {
+ var isVisible = entry.isIntersecting || entry.boundingClientRect.top < 200;
+ document.body.classList.toggle('sidebar-active', isVisible);
+ });
+ }, { threshold: 0.05 });
+ sidebarActiveObserver.observe(projectsSection);
+ } else if (pageCategory) {
+ // On subpages, always ensure sidebar is active
+ document.body.classList.add('sidebar-active');
+ }
+
+ /* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ SEARCH
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function getMatchingProjects(query) {
+ if (!query) return [];
+ var matches = [];
+ projectCards.forEach(function (card) {
+ var category = card.getAttribute('data-category');
+ var title = (card.querySelector('h3') || {}).textContent || '';
+ var desc = (card.querySelector('p') || {}).textContent || '';
+ var tags = (card.getAttribute('data-tags') || '').toLowerCase();
+ var q = query.toLowerCase();
+
+ var catMatch = currentCategory === 'all' || category === currentCategory;
+ var searchMatch = title.toLowerCase().includes(q) ||
+ desc.toLowerCase().includes(q) ||
+ tags.includes(q);
+
+ if (catMatch && searchMatch) {
+ matches.push({ card: card, title: title, tags: tags, category: category });
+ }
+ });
+ return matches;
+ }
+
+ function highlightText(container, text, query) {
+ var safe = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ var parts = text.split(new RegExp('(' + safe + ')', 'gi'));
+ parts.forEach(function (part) {
+ if (part.toLowerCase() === query.toLowerCase()) {
+ var mark = document.createElement('mark');
+ mark.style.background = 'var(--accent-soft)';
+ mark.style.color = 'var(--accent)';
+ mark.style.fontWeight = '600';
+ mark.style.borderRadius = '2px';
+ mark.style.padding = '0 2px';
+ mark.textContent = part;
+ container.appendChild(mark);
+ } else if (part) {
+ container.appendChild(document.createTextNode(part));
+ }
+ });
+ }
- function renderRecentSearches() {
- if (noResultsMessage) noResultsMessage.style.display = 'none';
- if (!recentSearchesSection) return;
-
- if (recentSearches.length === 0) {
- recentSearchesSection.style.display = 'none';
- if (tipsSection) tipsSection.style.display = 'block';
- if (resultsSection) resultsSection.style.display = 'none';
- return;
- }
+ function closeDropdown() {
+ if (searchDropdown) searchDropdown.classList.remove('active');
+ }
- if (recentSearchesList) {
- recentSearchesList.innerHTML = '';
- recentSearches.slice(0, 5).forEach(function (search) {
- var item = document.createElement('div');
- item.className = 'dropdown-recent-item';
- var recentText = document.createElement('div');
- recentText.className = 'dropdown-recent-text';
-
- var clockIcon = document.createElement('i');
- clockIcon.className = 'fas fa-history';
- clockIcon.style.opacity = '0.5';
- clockIcon.style.fontSize = '0.9rem';
-
- var searchLabel = document.createElement('span');
- searchLabel.style.flex = '1';
- searchLabel.style.cursor = 'pointer';
- searchLabel.style.color = 'var(--text-secondary)';
- searchLabel.textContent = search;
-
- recentText.append(clockIcon, searchLabel);
-
- var removeButton = document.createElement('button');
- removeButton.className = 'dropdown-recent-remove';
- removeButton.setAttribute('aria-label', 'Remove search');
-
- var removeIcon = document.createElement('i');
- removeIcon.className = 'fas fa-x';
- removeButton.appendChild(removeIcon);
-
- item.append(recentText, removeButton);
-
- searchLabel.addEventListener('click', function () {
- if (searchInput) {
- searchInput.value = search;
- currentSearchQuery = search;
- performSearch();
- closeDropdown();
- }
- });
-
- removeButton.addEventListener('click', function (e) {
- e.stopPropagation();
- recentSearches = recentSearches.filter(function (s) { return s !== search; });
- localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
- renderRecentSearches();
- });
-
- recentSearchesList.appendChild(item);
- });
- }
+ function renderRecentSearches() {
+ if (noResultsMessage) noResultsMessage.style.display = 'none';
+ if (!recentSearchesSection) return;
- recentSearchesSection.style.display = 'block';
- if (resultsSection) resultsSection.style.display = 'none';
- if (tipsSection) tipsSection.style.display = 'block';
+ if (recentSearches.length === 0) {
+ recentSearchesSection.style.display = 'none';
+ if (tipsSection) tipsSection.style.display = 'block';
+ if (resultsSection) resultsSection.style.display = 'none';
+ return;
}
- function renderSuggestions(query) {
- if (searchLoader) searchLoader.style.display = 'none';
- if (!query) { renderRecentSearches(); return; }
+ if (recentSearchesList) {
+ recentSearchesList.innerHTML = '';
+ recentSearches.slice(0, 5).forEach(function (search) {
+ var item = document.createElement('div');
+ item.className = 'dropdown-recent-item';
+ var text = document.createElement('div');
+ text.className = 'dropdown-recent-text';
- var matches = getMatchingProjects(query);
+ var clock = document.createElement('i');
+ clock.className = 'fas fa-history';
+ clock.style.opacity = '0.5';
+ clock.style.fontSize = '0.8rem';
- if (matches.length === 0) {
- if (resultsSection) resultsSection.style.display = 'none';
- if (recentSearchesSection) recentSearchesSection.style.display = 'none';
- if (tipsSection) tipsSection.style.display = 'block';
- if (noResultsMessage) noResultsMessage.style.display = 'block';
- return;
- }
-
- if (noResultsMessage) noResultsMessage.style.display = 'none';
-
- if (resultsList) {
- resultsList.innerHTML = '';
- matches.slice(0, 8).forEach(function (project, index) {
- var item = document.createElement('div');
- item.className = 'dropdown-item' + (index === selectedSuggestionIndex ? ' selected' : '');
- var bannerEl = project.card.querySelector('.card-banner');
- var iconBox = document.createElement('div');
- iconBox.className = 'dropdown-item-icon';
- if (bannerEl) {
- var img = document.createElement('img');
- img.src = bannerEl.src;
- img.alt = '';
- img.style.cssText = 'width:24px;height:24px;border-radius:4px;object-fit:cover';
- iconBox.appendChild(img);
- }
-
- var titleBox = document.createElement('div');
- titleBox.className = 'dropdown-item-text';
- highlightText(titleBox, project.title, query);
-
- var categoryTag = document.createElement('span');
- categoryTag.className = 'dropdown-item-tag';
- categoryTag.textContent = project.category;
-
- item.append(iconBox, titleBox, categoryTag);
- item.addEventListener('click', function () { selectSuggestion(project.title); });
- item.addEventListener('mouseenter', function () {
- selectedSuggestionIndex = index;
- updateSuggestionHighlight();
- });
- resultsList.appendChild(item);
- });
- }
+ var label = document.createElement('span');
+ label.textContent = search;
- if (resultsSection) resultsSection.style.display = 'block';
- if (recentSearchesSection) recentSearchesSection.style.display = 'none';
- if (tipsSection) tipsSection.style.display = 'none';
- selectedSuggestionIndex = -1;
- }
+ text.append(clock, label);
- if (searchInput) {
- var debouncedSearch = debounce(function (query) {
- renderSuggestions(query);
- }, 200);
-
- searchInput.addEventListener('input', function (e) {
- var query = e.target.value.trim().toLowerCase();
- currentSearchQuery = query;
- if (searchClear) searchClear.style.display = query ? 'flex' : 'none';
- if (searchLoader) searchLoader.style.display = query ? 'block' : 'none';
- debouncedSearch(query);
- performSearch();
- if (query && projectsSection && document.activeElement === searchInput) {
- projectsSection.scrollIntoView({
- behavior: prefersReducedMotion() ? 'auto' : 'smooth',
- block: 'start'
- });
- }
- });
+ var removeBtn = document.createElement('button');
+ removeBtn.className = 'dropdown-recent-remove';
+ removeBtn.setAttribute('aria-label', 'Remove search');
+ removeBtn.innerHTML = '
';
- searchInput.addEventListener('focus', function () {
- if (searchDropdown) searchDropdown.classList.add('active');
- if (searchShortcut) searchShortcut.style.display = 'none';
- if (!currentSearchQuery) renderRecentSearches();
- });
+ item.append(text, removeBtn);
- searchInput.addEventListener('keydown', function (e) {
- if (e.key === 'Escape') { closeDropdown(); searchInput.blur(); }
+ label.addEventListener('click', function () {
+ if (searchInput) searchInput.value = search;
+ currentSearchQuery = search;
+ performSearch();
+ closeDropdown();
});
- }
- if (searchClear) {
- searchClear.addEventListener('click', function () {
- if (searchInput) searchInput.value = '';
- currentSearchQuery = '';
- searchClear.style.display = 'none';
- if (searchLoader) searchLoader.style.display = 'none';
- cleanSearchHighlights();
- updateSearchResultCount(0);
- var hint = document.getElementById('emptyStateHint');
- if (hint) hint.textContent = 'Try adjusting your search or category filter.';
- applyCategoryFilter(currentCategory);
- closeDropdown();
+ removeBtn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ recentSearches = recentSearches.filter(function (s) { return s !== search; });
+ localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
+ renderRecentSearches();
});
- }
-
- document.addEventListener('click', function (e) {
- if (searchDropdown && searchInput &&
- !searchDropdown.contains(e.target) && e.target !== searchInput) {
- closeDropdown();
- }
- });
-
- document.addEventListener('keydown', function (e) {
- if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
- e.preventDefault();
- if (searchInput) searchInput.focus();
- }
- });
-
- renderRecentSearches();
- // โโ Init sidebar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- if (sidebarTabs.length) syncSidebarTabs('all');
-
- // โโ Central Dynamic Auto-Scaling (ResizeObserver) โโโโโโโโโโโโโโโโโ
- var modalResizeObserver = null;
-
- function applyModalScaling() {
- var modalContent = document.querySelector('.modal-content');
- var modalBody = document.getElementById('modalBody');
- if (!modalContent || !modalBody) return;
-
- modalContent.scrollTop = 0;
- modalBody.scrollTop = 0;
+ recentSearchesList.appendChild(item);
+ });
+ }
- modalContent.style.overflow = 'auto';
+ recentSearchesSection.style.display = 'block';
+ if (resultsSection) resultsSection.style.display = 'none';
+ if (tipsSection) tipsSection.style.display = 'block';
+ }
- modalBody.style.transform = '';
- modalBody.style.transformOrigin = '';
- modalBody.style.width = '100%';
- modalBody.style.height = '';
- modalBody.style.display = 'flex';
- modalBody.style.flexDirection = 'column';
- modalBody.style.alignItems = 'stretch';
- modalBody.style.gap = '1rem';
+ function renderSuggestions(query) {
+ if (searchLoader) searchLoader.style.display = 'none';
+ if (!query) { renderRecentSearches(); return; }
- var targetEl = Array.from(modalBody.children).find(function (el) {
- return el.tagName.toLowerCase() !== 'style';
- }) || modalBody.firstElementChild;
- if (!targetEl) return;
+ var matches = getMatchingProjects(query);
- targetEl.style.transform = '';
- targetEl.style.transformOrigin = '';
- targetEl.style.width = '100%';
- targetEl.style.maxWidth = '100%';
+ if (matches.length === 0) {
+ if (resultsSection) resultsSection.style.display = 'none';
+ if (recentSearchesSection) recentSearchesSection.style.display = 'none';
+ if (tipsSection) tipsSection.style.display = 'block';
+ if (noResultsMessage) noResultsMessage.style.display = 'block';
+ return;
}
- function initModalScaling() {
- applyModalScaling();
-
- if (modalResizeObserver) {
- modalResizeObserver.disconnect();
- }
-
- var modalBody = document.getElementById('modalBody');
- var targetEl = modalBody ? Array.from(modalBody.children).find(function (el) {
- return el.tagName.toLowerCase() !== 'style';
- }) || modalBody.firstElementChild : null;
- if (targetEl) {
- modalResizeObserver = new ResizeObserver(function () {
- requestAnimationFrame(applyModalScaling);
- });
- modalResizeObserver.observe(targetEl);
+ if (noResultsMessage) noResultsMessage.style.display = 'none';
+
+ if (resultsList) {
+ resultsList.innerHTML = '';
+ matches.slice(0, 8).forEach(function (project, index) {
+ var item = document.createElement('div');
+ item.className = 'dropdown-item' + (index === selectedSuggestionIndex ? ' selected' : '');
+
+ var iconBox = document.createElement('div');
+ iconBox.className = 'dropdown-item-icon';
+ var banner = project.card.querySelector('.card-banner');
+ if (banner) {
+ var img = document.createElement('img');
+ img.src = banner.src;
+ img.alt = '';
+ iconBox.appendChild(img);
}
- window.addEventListener('resize', applyModalScaling);
- }
+ var titleBox = document.createElement('div');
+ titleBox.className = 'dropdown-item-text';
+ highlightText(titleBox, project.title, query);
- function destroyModalScaling() {
- if (modalResizeObserver) {
- modalResizeObserver.disconnect();
- modalResizeObserver = null;
- }
- window.removeEventListener('resize', applyModalScaling);
- }
+ var tag = document.createElement('span');
+ tag.className = 'dropdown-item-tag';
+ tag.textContent = project.category;
- // โโ Focus Trap for Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- function getFocusableElements(root) {
- var selector =
- 'button:not([disabled]), [href], input:not([disabled]), ' +
- 'select:not([disabled]), textarea:not([disabled]), ' +
- '[tabindex]:not([tabindex="-1"])';
- return Array.from(root.querySelectorAll(selector)).filter(function (el) {
- return !el.closest('[aria-hidden="true"]') &&
- !el.classList.contains('visually-hidden');
+ item.append(iconBox, titleBox, tag);
+ item.addEventListener('click', function () { selectSuggestion(project.title); });
+ item.addEventListener('mouseenter', function () {
+ selectedSuggestionIndex = index;
+ updateSuggestionHighlight();
});
+ resultsList.appendChild(item);
+ });
}
- function trapFocus(modalEl) {
- var handler = function (e) {
- if (e.key !== 'Tab' || !modalEl.classList.contains('active')) return;
- var focusables = getFocusableElements(modalEl);
- if (!focusables.length) return;
- var first = focusables[0];
- var last = focusables[focusables.length - 1];
- if (e.shiftKey && document.activeElement === first) {
- e.preventDefault(); last.focus({ preventScroll: true });
- } else if (!e.shiftKey && document.activeElement === last) {
- e.preventDefault(); first.focus({ preventScroll: true });
- }
- };
- document.addEventListener('keydown', handler, true);
- return function () { document.removeEventListener('keydown', handler, true); };
+ if (resultsSection) resultsSection.style.display = 'block';
+ if (recentSearchesSection) recentSearchesSection.style.display = 'none';
+ if (tipsSection) tipsSection.style.display = 'none';
+ selectedSuggestionIndex = -1;
+ }
+
+ function updateSuggestionHighlight() {
+ if (!resultsList) return;
+ var items = resultsList.querySelectorAll('.dropdown-item');
+ items.forEach(function (item, i) {
+ item.classList.toggle('selected', i === selectedSuggestionIndex);
+ });
+ }
+
+ function selectSuggestion(title) {
+ if (!searchInput) return;
+ searchInput.value = title;
+ currentSearchQuery = title.toLowerCase();
+ performSearch();
+ closeDropdown();
+ if (projectsSection) {
+ projectsSection.scrollIntoView({ behavior: prefersReducedMotion() ? 'auto' : 'smooth', block: 'start' });
}
-
- // โโ Open / Close Project Modal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- /*
- * PLAYGROUND NOTE: openProjectSafe() is called synchronously from
- * card clicks. It NEVER touches Pyodide and NEVER waits for it.
- * The modal opens instantly regardless of Pyodide load state.
- */
- function openProjectSafe(name, trigger) {
- if (!modal || !modalBody) return;
-
- lastFocusedElement = trigger || document.activeElement;
-
- if (modalTitle) modalTitle.textContent = name || 'Interactive project';
-
- modal.classList.add('active');
- modal.setAttribute('aria-hidden', 'false');
-
- // Prevent scroll shift by adding padding equal to scrollbar width
- var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
- document.body.style.paddingRight = scrollbarWidth + 'px';
- document.body.style.overflow = 'hidden';
- setMainInert(true);
-
- safeRun(function () {
- if (typeof getProjectHTML === 'function') {
- modalBody.innerHTML =
- getProjectHTML(name) ||
- '
Project content unavailable.
';
- } else {
- modalBody.innerHTML = '
Project content unavailable.
';
- }
- if (typeof initializeProject === 'function') initializeProject(name);
- });
-
- // Initialize reactive scale calculations
- initModalScaling();
-
- removeTrap = trapFocus(modal);
- var focusables = getFocusableElements(modalBody);
- var firstFocusable = focusables[0] || modalClose;
- if (firstFocusable && typeof firstFocusable.focus === 'function') {
- firstFocusable.focus({ preventScroll: true });
- }
+ }
+
+ function performSearch() {
+ var query = currentSearchQuery;
+ if (!query) {
+ applyCategoryFilter(currentCategory);
+ if (emptyStateHint) emptyStateHint.textContent = 'Try adjusting your search or category filter.';
+ return;
}
- function closeProjectSafe() {
- if (!modal || !modal.classList.contains('active')) return;
-
- destroyModalScaling();
-
- modal.classList.remove('active');
- modal.setAttribute('aria-hidden', 'true');
- document.body.style.paddingRight = '';
- document.body.style.overflow = '';
- setMainInert(false);
- if (removeTrap) { removeTrap(); removeTrap = null; }
- if (modalBody) {
- modalBody.innerHTML = '';
- modalBody.style.transform = '';
- modalBody.style.transformOrigin = '';
- modalBody.style.width = '';
- modalBody.style.height = '';
- modalBody.style.display = '';
- modalBody.style.alignItems = '';
- modalBody.style.gap = '';
- }
- if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
- lastFocusedElement.focus({ preventScroll: true });
- }
- lastFocusedElement = null;
+ if (currentCategory !== 'all') {
+ currentCategory = 'all';
+ syncSidebarTabs('all');
+ syncStickyTabs('all');
}
- if (modalClose) modalClose.addEventListener('click', closeProjectSafe);
- if (modal) {
- modal.addEventListener('click', function (e) {
- if (e.target === modal) closeProjectSafe();
- });
- }
- document.addEventListener('keydown', function (e) {
- if (e.key === 'Escape') closeProjectSafe();
- });
+ recentSearches = recentSearches.filter(function (s) { return s !== query; });
+ recentSearches.unshift(query);
+ recentSearches = recentSearches.slice(0, 10);
+ localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
- // โโ Wire Cards and Play Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ var visibleCount = 0;
+ var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
projectCards.forEach(function (card) {
- var name = card.getAttribute('data-project');
- var categoryName = card.getAttribute('data-category') || 'project';
- var metaRow = document.createElement('div');
- metaRow.className = 'project-card-meta';
-
- var categoryBadge = document.createElement('span');
- categoryBadge.className = 'project-card-badge';
- categoryBadge.textContent = categoryName;
- metaRow.appendChild(categoryBadge);
-
- var favBtn = document.createElement('button');
- favBtn.className = 'btn-favorite';
- favBtn.setAttribute('aria-label', 'Toggle favorite');
- favBtn.textContent = '\u2606';
-
- var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
- if (favorites.includes(name)) {
- favBtn.classList.add('active');
- favBtn.textContent = '\u2605';
- }
-
- favBtn.addEventListener('click', function (e) {
- e.stopPropagation();
- var favs = JSON.parse(localStorage.getItem('favorites') || '[]');
- var idx = favs.indexOf(name);
- if (idx === -1) {
- favs.push(name);
- favBtn.classList.add('active');
- favBtn.textContent = '\u2605';
- } else {
- favs.splice(idx, 1);
- favBtn.classList.remove('active');
- favBtn.textContent = '\u2606';
- if (currentCategory === 'favorites') {
- card.style.display = 'none';
- }
- }
- localStorage.setItem('favorites', JSON.stringify(favs));
- });
- var cardActions = card.querySelector('.card-actions');
- if (cardActions) {
- cardActions.appendChild(favBtn);
- } else {
- card.appendChild(favBtn);
- }
-
- var play = card.querySelector('.btn-play');
- if (play) {
- play.setAttribute('aria-label', 'Open ' + name);
- play.addEventListener('click', function (e) {
- e.stopPropagation();
- openProjectSafe(name, play);
- });
- }
- card.appendChild(metaRow);
- card.addEventListener('click', function () { openProjectSafe(name, card); });
+ var category = card.getAttribute('data-category');
+ var title = (card.querySelector('h3') || {}).textContent || '';
+ var desc = (card.querySelector('p') || {}).textContent || '';
+ var tags = (card.getAttribute('data-tags') || '').toLowerCase();
+ var projectName = card.getAttribute('data-project');
+ var isFav = favorites.includes(projectName);
+
+ var catMatch = (currentCategory === 'all') ||
+ (currentCategory === 'favorites' && isFav) ||
+ (currentCategory !== 'favorites' && category === currentCategory);
+ var searchMatch = title.toLowerCase().includes(query) ||
+ desc.toLowerCase().includes(query) ||
+ tags.includes(query);
+
+ if (catMatch && searchMatch) {
+ card.style.display = '';
+ visibleCount++;
+ } else {
+ card.style.display = 'none';
+ }
});
- // โโ Random button is in the sidebar (#randomProjectBtnSidebar) โโโโโ
+ if (emptyState) {
+ emptyState.style.display = visibleCount === 0 ? 'block' : 'none';
+ if (visibleCount === 0 && emptyStateHint) {
+ emptyStateHint.textContent = 'No projects match "' + query + '". Try a different keyword.';
+ }
+ }
+ if (projectCountBadge) projectCountBadge.textContent = String(visibleCount) + ' projects';
+ }
+
+ if (searchInput) {
+ var debouncedSearch = debounce(function (query) {
+ renderSuggestions(query);
+ }, 200);
+
+ searchInput.addEventListener('input', function (e) {
+ var query = e.target.value.trim().toLowerCase();
+ currentSearchQuery = query;
+ if (searchLoader) searchLoader.style.display = query ? 'block' : 'none';
+ debouncedSearch(query);
+ performSearch();
+ });
- featureLaunchers.forEach(function (node) {
- node.addEventListener('click', function (e) {
- if (e.target.closest('[data-project-target]') === node) {
- var targetProject = node.getAttribute('data-project-target');
- if (targetProject) openProjectSafe(targetProject, node);
- }
- });
+ searchInput.addEventListener('focus', function () {
+ if (searchDropdown) searchDropdown.classList.add('active');
+ if (!currentSearchQuery) renderRecentSearches();
});
- if (!prefersReducedMotion()) {
- projectCards.forEach(function (card) {
- card.addEventListener('mousemove', function (event) {
- var rect = card.getBoundingClientRect();
- var px = (event.clientX - rect.left) / rect.width;
- var py = (event.clientY - rect.top) / rect.height;
- var rotateY = (px - 0.5) * 10;
- var rotateX = (0.5 - py) * 8;
- card.style.transform = 'perspective(900px) rotateX(' + rotateX + 'deg) rotateY(' + rotateY + 'deg) translateY(-8px)';
- });
- card.addEventListener('mouseleave', function () {
- card.style.transform = '';
- });
- });
+ searchInput.addEventListener('keydown', function (e) {
+ if (e.key === 'Escape') { closeDropdown(); searchInput.blur(); }
+ });
+ }
- var parallaxItems = document.querySelectorAll('[data-parallax]');
- window.addEventListener('scroll', function () {
- var scrollY = window.scrollY;
- parallaxItems.forEach(function (item) {
- var ratio = parseFloat(item.getAttribute('data-parallax') || '0');
- item.style.transform = 'translateY(' + Math.round(scrollY * ratio) + 'px)';
- });
- }, { passive: true });
+ document.addEventListener('click', function (e) {
+ if (searchDropdown && searchInput &&
+ !searchDropdown.contains(e.target) && e.target !== searchInput) {
+ closeDropdown();
}
+ });
- // โโ Intersection Observer Animations โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- if (!prefersReducedMotion()) {
- try {
- var observer = new IntersectionObserver(function (entries) {
- entries.forEach(function (entry) {
- if (entry.isIntersecting) {
- entry.target.style.animation = 'fadeInUp 0.6s ease';
- }
- });
- }, { threshold: 0.1, rootMargin: '0px 0px -100px 0px' });
- projectCards.forEach(function (c) { observer.observe(c); });
- } catch (e) { /* ignore */ }
+ document.addEventListener('keydown', function (e) {
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ if (searchInput) searchInput.focus();
}
+ });
- if (!prefersReducedMotion()) {
- try {
- var revealObserver = new IntersectionObserver(function (entries, obs) {
- entries.forEach(function (entry) {
- if (!entry.isIntersecting) return;
- entry.target.classList.add('is-visible');
- obs.unobserve(entry.target);
- });
- }, { threshold: 0.2 });
-
- revealItems.forEach(function (item) { revealObserver.observe(item); });
- } catch (e) { /* ignore */ }
- } else {
- revealItems.forEach(function (item) { item.classList.add('is-visible'); });
- }
+ renderRecentSearches();
- // โโ Hero Timeline scroll reveal โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- var timelineNodes = document.querySelectorAll('.timeline-node[data-reveal]');
- var heroTimeline = document.getElementById('heroTimeline');
+ /* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ MODAL
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function getFocusableElements(root) {
+ var sel = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
+ return Array.from(root.querySelectorAll(sel)).filter(function (el) {
+ return !el.closest('[aria-hidden="true"]') && !el.classList.contains('visually-hidden');
+ });
+ }
+
+ function trapFocus(modalEl) {
+ var handler = function (e) {
+ if (e.key !== 'Tab' || !modalEl.classList.contains('active')) return;
+ var focusables = getFocusableElements(modalEl);
+ if (!focusables.length) return;
+ var first = focusables[0];
+ var last = focusables[focusables.length - 1];
+ if (e.shiftKey && document.activeElement === first) {
+ e.preventDefault(); last.focus({ preventScroll: true });
+ } else if (!e.shiftKey && document.activeElement === last) {
+ e.preventDefault(); first.focus({ preventScroll: true });
+ }
+ };
+ document.addEventListener('keydown', handler, true);
+ return function () { document.removeEventListener('keydown', handler, true); };
+ }
+
+ function openProjectSafe(name, trigger) {
+ if (!modal || !modalBody) return;
+ lastFocusedElement = trigger || document.activeElement;
+ if (modalTitle) modalTitle.textContent = name || 'Interactive project';
+ modal.classList.add('active');
+ modal.setAttribute('aria-hidden', 'false');
+ var scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+ document.body.style.paddingRight = scrollbarWidth + 'px';
+ document.body.style.overflow = 'hidden';
+ setMainInert(true);
+
+ safeRun(function () {
+ if (typeof getProjectHTML === 'function') {
+ modalBody.innerHTML = getProjectHTML(name) || '
Project content unavailable.
';
+ } else {
+ modalBody.innerHTML = '
Project content unavailable.
';
+ }
+ if (typeof initializeProject === 'function') initializeProject(name);
+ });
- function updateSpineProgress() {
- if (!heroTimeline) return;
- var revealed = document.querySelectorAll('.timeline-node.revealed').length;
- var total = timelineNodes.length;
- var pct = total > 0 ? (revealed / total) * 100 : 0;
- heroTimeline.style.setProperty('--spine-progress', pct);
+ removeTrap = trapFocus(modal);
+ var focusables = getFocusableElements(modalBody);
+ var firstFocusable = focusables[0] || modalClose;
+ if (firstFocusable && typeof firstFocusable.focus === 'function') {
+ firstFocusable.focus({ preventScroll: true });
+ }
+ }
+
+ function closeProjectSafe() {
+ if (!modal || !modal.classList.contains('active')) return;
+ modal.classList.remove('active');
+ modal.setAttribute('aria-hidden', 'true');
+ document.body.style.paddingRight = '';
+ document.body.style.overflow = '';
+ setMainInert(false);
+ if (removeTrap) { removeTrap(); removeTrap = null; }
+ if (modalBody) {
+ modalBody.innerHTML = '';
}
+ if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
+ lastFocusedElement.focus({ preventScroll: true });
+ }
+ lastFocusedElement = null;
+ }
- if (timelineNodes.length) {
- timelineNodes[0].classList.add('revealed');
- updateSpineProgress();
+ if (modalClose) modalClose.addEventListener('click', closeProjectSafe);
+ if (modal) {
+ modal.addEventListener('click', function (e) {
+ if (e.target === modal) closeProjectSafe();
+ });
+ }
+ document.addEventListener('keydown', function (e) {
+ if (e.key === 'Escape') closeProjectSafe();
+ });
+
+ /* โโ Expose for inline use โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ window.openProjectSafe = openProjectSafe;
+ window.closeProjectSafe = closeProjectSafe;
+
+ /* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ WIRE PROJECT CARDS
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ projectCards.forEach(function (card) {
+ var name = card.getAttribute('data-project');
+
+ /* โโ Favorite Button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var favBtn = document.createElement('button');
+ favBtn.className = 'btn-favorite';
+ favBtn.setAttribute('aria-label', 'Toggle favorite');
+ favBtn.innerHTML = '
';
+
+ var favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
+ if (favorites.includes(name)) {
+ favBtn.classList.add('active');
+ favBtn.innerHTML = '
';
}
- if (timelineNodes.length > 1 && !prefersReducedMotion()) {
- var revealedCount = 1;
- var scrollAccum = 0;
- var prevScrollY = window.scrollY;
-
- window.addEventListener('scroll', function () {
- var currentScrollY = window.scrollY;
- var delta = currentScrollY - prevScrollY;
- scrollAccum += delta;
- prevScrollY = currentScrollY;
-
- if (scrollAccum >= 60) {
- while (scrollAccum >= 60 && revealedCount < timelineNodes.length) {
- timelineNodes[revealedCount].classList.add('revealed');
- revealedCount++;
- scrollAccum -= 60;
- }
- } else if (scrollAccum <= -60) {
- while (scrollAccum <= -60 && revealedCount > 1) {
- revealedCount--;
- timelineNodes[revealedCount].classList.remove('revealed');
- scrollAccum += 60;
- }
- }
- updateSpineProgress();
- }, { passive: true });
- } else if (timelineNodes.length > 1) {
- for (var i = 1; i < timelineNodes.length; i++) {
- timelineNodes[i].classList.add('revealed');
+ favBtn.addEventListener('click', function (e) {
+ e.stopPropagation();
+ var favs = JSON.parse(localStorage.getItem('favorites') || '[]');
+ var idx = favs.indexOf(name);
+ if (idx === -1) {
+ favs.push(name);
+ favBtn.classList.add('active');
+ favBtn.innerHTML = '
';
+ } else {
+ favs.splice(idx, 1);
+ favBtn.classList.remove('active');
+ favBtn.innerHTML = '
';
+ if (currentCategory === 'favorites') {
+ card.style.display = 'none';
}
- updateSpineProgress();
- }
+ }
+ localStorage.setItem('favorites', JSON.stringify(favs));
+ });
- // โโ Share Button Feature โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ var cardActions = card.querySelector('.card-actions');
+ if (cardActions) cardActions.appendChild(favBtn);
-// 1. Inject share button into every card dynamically
-projectCards.forEach(function (card) {
- var projectName = card.getAttribute('data-project');
+ /* โโ Share Button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
var shareBtn = document.createElement('button');
shareBtn.className = 'btn-share';
- shareBtn.setAttribute('aria-label', 'Share ' + projectName);
+ shareBtn.setAttribute('aria-label', 'Share ' + name);
shareBtn.innerHTML = '
';
shareBtn.title = 'Copy shareable link';
shareBtn.addEventListener('click', function (e) {
- e.stopPropagation(); // prevent card click opening modal
- var url = window.location.origin + window.location.pathname + '?project=' + encodeURIComponent(projectName);
- navigator.clipboard.writeText(url).then(function () {
- showToast('Link copied!');
- }).catch(function () {
- // Fallback for browsers that block clipboard
- showToast('Copy this: ' + url);
- });
+ e.stopPropagation();
+ var url = window.location.origin + window.location.pathname + '?project=' + encodeURIComponent(name);
+ navigator.clipboard.writeText(url).then(function () {
+ showToast('Link copied!');
+ }).catch(function () {
+ showToast('Copy this: ' + url);
+ });
});
- var cardActions = card.querySelector('.card-actions');
- if (cardActions) {
- cardActions.appendChild(shareBtn);
- } else {
- card.appendChild(shareBtn);
+ if (cardActions) cardActions.appendChild(shareBtn);
+
+ /* โโ Play Button โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var playBtns = card.querySelectorAll('.btn-play');
+ playBtns.forEach(function (play) {
+ play.setAttribute('aria-label', 'Open ' + name);
+ play.addEventListener('click', function (e) {
+ e.stopPropagation();
+ openProjectSafe(name, play);
+ });
+ });
+
+ /* โโ Card Click โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ card.addEventListener('click', function (e) {
+ if (e.target.closest('.btn-play') || e.target.closest('.btn-favorite') || e.target.closest('.btn-share')) return;
+ openProjectSafe(name, card);
+ });
+
+ /* โโ Card Mouse Tracking for Border Glow โโโโโโโโโโโโโโโโ */
+ if (!prefersReducedMotion()) {
+ card.addEventListener('mousemove', function (e) {
+ var rect = card.getBoundingClientRect();
+ var x = ((e.clientX - rect.left) / rect.width) * 100;
+ var y = ((e.clientY - rect.top) / rect.height) * 100;
+ card.style.setProperty('--mouse-x', x + '%');
+ card.style.setProperty('--mouse-y', y + '%');
+ });
}
-});
+ });
-// 2. Toast notification helper
-function showToast(message) {
+ /* โโ Toast โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function showToast(message) {
var existing = document.getElementById('shareToast');
if (existing) existing.remove();
-
var toast = document.createElement('div');
toast.id = 'shareToast';
toast.className = 'share-toast';
toast.textContent = message;
document.body.appendChild(toast);
-
- // Trigger animation
requestAnimationFrame(function () {
- toast.classList.add('share-toast--visible');
+ toast.classList.add('share-toast--visible');
});
-
setTimeout(function () {
- toast.classList.remove('share-toast--visible');
- setTimeout(function () { toast.remove(); }, 300);
+ toast.classList.remove('share-toast--visible');
+ setTimeout(function () { toast.remove(); }, 300);
}, 2500);
-}
+ }
-// 3. On page load, check for ?project= param and auto-open it
-(function () {
+ /* โโ URL params auto-open โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ (function () {
var params = new URLSearchParams(window.location.search);
var projectParam = params.get('project');
- if (!projectParam) return;
+ if (projectParam) {
+ var match = projectCards.find(function (c) { return c.getAttribute('data-project') === projectParam; });
+ if (match) {
+ setTimeout(function () {
+ openProjectSafe(projectParam, match);
+ match.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 300);
+ }
+ }
+ var catParam = params.get('category');
+ var valid = ['all', 'games', 'math', 'utilities', 'playground', 'favorites'];
+ if (catParam && valid.includes(catParam)) {
+ var tab = document.querySelector('[data-category="' + catParam + '"]');
+ if (tab) setTimeout(function () { tab.click(); }, 100);
+ }
+ })();
+
+ /* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ TIMELINE SCROLL REVEAL
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var timelineItems = document.querySelectorAll('.timeline-item[data-reveal]');
+ var timelineFill = document.getElementById('timelineFill');
+ var timelineSection = document.getElementById('timelineSection');
+
+ if (timelineItems.length && !prefersReducedMotion()) {
+ var timelineObserver = new IntersectionObserver(function (entries) {
+ entries.forEach(function (entry) {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('visible');
+ }
+ });
+ }, { threshold: 0.25, rootMargin: '0px 0px -50px 0px' });
- var matchingCard = projectCards.find(function (card) {
- return card.getAttribute('data-project') === projectParam;
+ timelineItems.forEach(function (item) {
+ timelineObserver.observe(item);
});
- if (matchingCard) {
- setTimeout(function () {
- var projectName = matchingCard.getAttribute('data-project');
- openProjectSafe(projectName, matchingCard);
- matchingCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }, 300); // small delay so the page fully loads first
+ /* โโ Serpentine SVG Winding Timeline Path โโโโโโโโโโโโโโโโโโโ */
+ var svgNamespace = 'http://www.w3.org/2000/svg';
+
+ function getElementOffset(el, parent) {
+ var top = 0;
+ var left = 0;
+ var curr = el;
+ while (curr && curr !== parent) {
+ top += curr.offsetTop || 0;
+ left += curr.offsetLeft || 0;
+ curr = curr.offsetParent;
+ }
+ return { top: top, left: left };
}
-})();
-// 4. On page load, check for ?category= param and apply filter
-(function () {
- var params = new URLSearchParams(window.location.search);
- var categoryParam = params.get('category');
- var validCategories = ['all', 'games', 'math', 'utilities', 'playground', 'favorites'];
- if (!categoryParam || !validCategories.includes(categoryParam)) return;
+ function rebuildTimelineSvg() {
+ var container = document.querySelector('.timeline-container');
+ if (!container) return;
+ var dots = document.querySelectorAll('.timeline-dot');
+ if (dots.length < 2) return;
- var matchingTab = document.querySelector('[data-category="' + categoryParam + '"]');
- if (matchingTab) {
- setTimeout(function () {
- matchingTab.click();
- }, 100);
+ var containerWidth = container.offsetWidth;
+ var containerHeight = container.offsetHeight;
+
+ var svg = document.getElementById('timelineSvg');
+ if (!svg) {
+ svg = document.createElementNS(svgNamespace, 'svg');
+ svg.id = 'timelineSvg';
+ svg.setAttribute('class', 'timeline-svg');
+
+ var defs = document.createElementNS(svgNamespace, 'defs');
+ var grad = document.createElementNS(svgNamespace, 'linearGradient');
+ grad.id = 'timelineGrad';
+ grad.setAttribute('x1', '0%');
+ grad.setAttribute('y1', '0%');
+ grad.setAttribute('x2', '0%');
+ grad.setAttribute('y2', '100%');
+
+ var stop1 = document.createElementNS(svgNamespace, 'stop');
+ stop1.setAttribute('offset', '0%');
+ stop1.setAttribute('stop-color', 'var(--accent)');
+
+ var stop2 = document.createElementNS(svgNamespace, 'stop');
+ stop2.setAttribute('offset', '100%');
+ stop2.setAttribute('stop-color', '#06b6d4');
+
+ grad.appendChild(stop1);
+ grad.appendChild(stop2);
+ defs.appendChild(grad);
+
+ // Define a dynamic layout mask path for progress crawling
+ var mask = document.createElementNS(svgNamespace, 'mask');
+ mask.id = 'timelineMask';
+
+ var maskPath = document.createElementNS(svgNamespace, 'path');
+ maskPath.id = 'timelineMaskPath';
+ maskPath.setAttribute('fill', 'none');
+ maskPath.setAttribute('stroke', '#ffffff');
+ maskPath.setAttribute('stroke-width', '24'); // Wide enough to fully cover glowing dots
+ maskPath.setAttribute('stroke-linecap', 'round');
+
+ mask.appendChild(maskPath);
+ defs.appendChild(mask);
+ svg.appendChild(defs);
+
+ var track = document.createElementNS(svgNamespace, 'path');
+ track.id = 'timelineSvgTrack';
+ track.setAttribute('class', 'timeline-svg-track');
+ track.setAttribute('fill', 'none');
+
+ var fill = document.createElementNS(svgNamespace, 'path');
+ fill.id = 'timelineSvgFill';
+ fill.setAttribute('class', 'timeline-svg-fill');
+ fill.setAttribute('fill', 'none');
+ fill.setAttribute('stroke', 'var(--accent)');
+ fill.setAttribute('mask', 'url(#timelineMask)');
+
+ svg.appendChild(track);
+ svg.appendChild(fill);
+
+ var grid = document.querySelector('.timeline-grid');
+ container.insertBefore(svg, grid);
+ }
+
+ // Determine layout stable coordinate points for all timeline dots
+ var coords = [];
+ dots.forEach(function (dot) {
+ var offset = getElementOffset(dot, container);
+ var x = offset.left + dot.offsetWidth / 2;
+ var y = offset.top + dot.offsetHeight / 2;
+ coords.push({ x: x, y: y });
+ });
+
+ // Create the winding path
+ var d = "";
+ var startX = containerWidth / 2;
+ d += "M " + startX + " 0";
+ d += " L " + coords[0].x + " " + coords[0].y;
+
+ // Calculate a sweep width that is perfectly responsive
+ // e.g. 16% of container width, capped at 180px for desktop beauty
+ var W = Math.min(180, containerWidth * 0.16);
+
+ for (var i = 0; i < coords.length - 1; i++) {
+ var pStart = coords[i];
+ var pEnd = coords[i + 1];
+ var H = pEnd.y - pStart.y;
+ var dy = H * 0.35; // Symmetrical control point height
+
+ // Even segments (0, 2, 4...) snake to the right, odd segments to the left
+ var dx = (i % 2 === 0) ? W : -W;
+
+ var cp1x = pStart.x + dx;
+ var cp1y = pStart.y + dy;
+ var cp2x = pEnd.x + dx;
+ var cp2y = pEnd.y - dy;
+
+ d += " C " + cp1x + " " + cp1y + ", " + cp2x + " " + cp2y + ", " + pEnd.x + " " + pEnd.y;
+ }
+
+ // Straight exit to the bottom
+ d += " L " + coords[coords.length - 1].x + " " + containerHeight;
+
+ var trackPath = document.getElementById('timelineSvgTrack');
+ var fillPath = document.getElementById('timelineSvgFill');
+ var maskPath = document.getElementById('timelineMaskPath');
+ if (trackPath && fillPath && maskPath) {
+ trackPath.setAttribute('d', d);
+ fillPath.setAttribute('d', d);
+ maskPath.setAttribute('d', d);
+
+ var totalLength = maskPath.getTotalLength();
+ maskPath.style.strokeDasharray = totalLength;
+ maskPath.dataset.totalLength = totalLength;
+
+ // Trigger scroll progress sync immediately
+ updateTimelineFill();
+ }
}
-})();
+
+ /* โโ Timeline Fill Progress โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ function updateTimelineFill() {
+ if (!timelineSection) return;
+ var container = document.querySelector('.timeline-container');
+ if (!container) return;
+
+ var containerRect = container.getBoundingClientRect();
+ var viewportCenterY = window.innerHeight / 2;
+
+ // Calculate relative vertical scroll position of the viewport center relative to the container
+ var relativeY = viewportCenterY - containerRect.top;
+ var offset = Math.max(0, Math.min(1, relativeY / containerRect.height));
+
+ /* โโ Dynamic SVG path mask scroll synchronization โโโโโโโโ */
+ var maskPath = document.getElementById('timelineMaskPath');
+ if (maskPath && maskPath.dataset.totalLength) {
+ var totalLength = parseFloat(maskPath.dataset.totalLength);
+ var dashoffset = totalLength - (offset * totalLength);
+ maskPath.style.strokeDashoffset = Math.max(0, Math.min(totalLength, dashoffset));
+ }
+
+ /* โโ Activate item based on viewport center crossing timeline dots โโ */
+ var activeIdx = -1;
+ var dots = document.querySelectorAll('.timeline-dot');
+
+ dots.forEach(function (dot, i) {
+ var dotRect = dot.getBoundingClientRect();
+ var dotCenterY = dotRect.top + dotRect.height / 2;
+
+ // A dot is crossed/passed if its vertical center in the viewport is <= the viewport center
+ if (dotCenterY <= viewportCenterY) {
+ activeIdx = i;
+ }
+ });
+
+ timelineItems.forEach(function (item, i) {
+ item.classList.toggle('active', i === activeIdx);
+ });
+ }
+
+ // Initialize SVG path layout recalculations on page render & resize
+ rebuildTimelineSvg();
+ window.addEventListener('resize', debounce(rebuildTimelineSvg, 150));
+ window.addEventListener('scroll', updateTimelineFill, { passive: true });
+ } else if (timelineItems.length) {
+ timelineItems.forEach(function (item) { item.classList.add('visible'); });
+ }
+
+ /* โโ Reveal on Scroll (general) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ var revealItems = document.querySelectorAll('.reveal-on-scroll');
+ if (revealItems.length && !prefersReducedMotion()) {
+ var revealObserver = new IntersectionObserver(function (entries, obs) {
+ entries.forEach(function (entry) {
+ if (!entry.isIntersecting) return;
+ entry.target.classList.add('is-visible');
+ obs.unobserve(entry.target);
+ });
+ }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' });
+ revealItems.forEach(function (item) { revealObserver.observe(item); });
+ } else {
+ revealItems.forEach(function (item) { item.classList.add('is-visible'); });
+ }
+
+ /* โโ Footer category links โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+ document.querySelectorAll('.footer-cat-link').forEach(function (a) {
+ a.addEventListener('click', function (e) {
+ e.preventDefault();
+ var cat = a.getAttribute('data-cat');
+ var tab = document.querySelector('.sidebar-tab[data-category="' + cat + '"]');
+ if (tab) tab.click();
+ });
+ });
});
diff --git a/web-app/js/projects.js b/web-app/js/projects.js
index 04ba8fd..3084f6b 100644
--- a/web-app/js/projects.js
+++ b/web-app/js/projects.js
@@ -1,4 +1,4 @@
-// Project Registry
+๏ปฟ// Project Registry
// Each project's HTML and logic lives in its own file under js/projects/
@@ -39,6 +39,7 @@ function getProjectHTML(projectName) {
'2048-game': () => get2048GameHTML(),
'productive-pet': () => getProductivePetHTML(),
'color-palette': () => getColorPaletteHTML(),
+ 'caesar-cipher': () => getCaesarCipherHTML(),
};
try {
@@ -3022,187 +3023,541 @@ function initTowerOfHanoi() {
initializeGame();
}
-function getTicTacToeHTML() {
- return `
-
-
-
-
Tic Tac Toe
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Player X's Turn
+// ============================================
+// Tic-Tac-Toe
+// ============================================
-
-
- `;
+function getTicTacToeHTML() {
+ return `
+
+
+
+
+
+ X
+ ยท
+ O
+
+
Tic Tac Toe
+
Two players or vs AI โ classic strategy game!
+
+
+
Game Mode
+
+
+
+
+
+
+
+
Difficulty
+
+
+
+
+
+
+
+
+
Rounds
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Round 1 of 3
+
+
+
+ โ
+ Player 1's turn
+
+
+
+
+
+
+ ${[0,1,2,3,4,5,6,7,8].map(i =>
+ ``
+ ).join('')}
+
+
+
+
+
+
+
+
+
๐
+
Player 1 wins!
+
+
+
+
+
+
+
+
+
+
๐
+
Player 1 Wins!
+
+ P1
+ 0
+ โ
+ 0
+ P2
+
+
0 draws
+
+
+
+
+
+
+
+`;
}
-
-let currentPlayer = 'X';
-
-let board = [
- '', '', '',
- '', '', '',
- '', '', ''
-];
-
-let gameActive = true;
-
-const winningPatterns = [
- [0,1,2],
- [3,4,5],
- [6,7,8],
-
- [0,3,6],
- [1,4,7],
- [2,5,8],
-
- [0,4,8],
- [2,4,6]
-];
-
-function makeMove(index) {
-
- if (!gameActive || board[index] !== '') {
- return;
+function initTicTacToe() {
+
+ // Win combos
+ const WINS = [
+ [0,1,2],[3,4,5],[6,7,8], // rows
+ [0,3,6],[1,4,7],[2,5,8], // cols
+ [0,4,8],[2,4,6] // diagonals
+ ];
+
+ // Win-line centre coordinates (column, row) in 0-2 grid space
+ const WIN_COORDS = [
+ [[0,0],[2,0]], [[0,1],[2,1]], [[0,2],[2,2]], // rows
+ [[0,0],[0,2]], [[1,0],[1,2]], [[2,0],[2,2]], // cols
+ [[0,0],[2,2]], [[2,0],[0,2]] // diagonals
+ ];
+
+ // โโ State โโ
+ let mode = "2p";
+ let difficulty = "easy";
+ let maxRounds = 3;
+ let p1 = "Player 1";
+ let p2 = "Player 2";
+ let scores = { p1:0, p2:0, draws:0 };
+ let board = [];
+ let current = "X"; // "X" | "O"
+ let round = 1;
+ let gameOver = false;
+
+ // โโ Helpers โโ
+ function qs(sel, ctx) { return (ctx||document).querySelector(sel); }
+
+ // Show one of the three screens
+ function showScreen(id) {
+ ["ttt-setup","ttt-game","ttt-final"].forEach(s => {
+ const el = document.getElementById(s);
+ if (el) {
+ el.classList.toggle("ttt-screen--active", s === id);
+ }
+ });
+ }
+
+ // Pill-toggle group helper
+ function initPillGroup(groupId, onChange) {
+ const grp = document.getElementById(groupId);
+ if (!grp) return;
+ grp.querySelectorAll(".ttt-pill").forEach(btn => {
+ btn.addEventListener("click", () => {
+ grp.querySelectorAll(".ttt-pill").forEach(b => b.classList.remove("ttt-pill--on"));
+ btn.classList.add("ttt-pill--on");
+ onChange(btn.dataset.val);
+ });
+ });
+ }
+
+ // โโ Wire up Setup โโ
+ initPillGroup("ttt-mode-group", val => {
+ mode = val;
+ const diffGroup = document.getElementById("ttt-diff-group");
+ const p2box = document.getElementById("ttt-p2-box");
+ const p2inp = document.getElementById("ttt-p2");
+ if (val === "ai") {
+ diffGroup.style.display = "block";
+ p2box.classList.add("ttt-dimmed");
+ p2inp.disabled = true;
+ p2inp.placeholder = "Computer ๐ค";
+ } else {
+ diffGroup.style.display = "none";
+ p2box.classList.remove("ttt-dimmed");
+ p2inp.disabled = false;
+ p2inp.placeholder = "Player 2";
}
+ });
+
+ initPillGroup("ttt-diff-pills", val => { difficulty = val; });
+ initPillGroup("ttt-rounds-group", val => { maxRounds = parseInt(val); });
+
+ // Start button
+ const startBtn = document.getElementById("ttt-start");
+ if (startBtn) {
+ startBtn.addEventListener("click", () => {
+ p1 = (document.getElementById("ttt-p1").value.trim()) || "Player 1";
+ p2 = mode === "ai"
+ ? "Computer ๐ค"
+ : ((document.getElementById("ttt-p2").value.trim()) || "Player 2");
+ scores = { p1:0, p2:0, draws:0 };
+ round = 1;
+ newRound();
+ showScreen("ttt-game");
+ });
+ }
+
+ // Back / Menu buttons
+ const backBtn = document.getElementById("ttt-back");
+ if (backBtn) backBtn.addEventListener("click", () => showScreen("ttt-setup"));
+
+ const menuBtn = document.getElementById("ttt-menu");
+ if (menuBtn) menuBtn.addEventListener("click", () => showScreen("ttt-setup"));
+
+ // Rematch button
+ const rematchBtn = document.getElementById("ttt-rematch");
+ if (rematchBtn) {
+ rematchBtn.addEventListener("click", () => {
+ scores = { p1:0, p2:0, draws:0 };
+ round = 1;
+ newRound();
+ showScreen("ttt-game");
+ });
+ }
+
+ // Next-round button
+ const nextBtn = document.getElementById("ttt-next");
+ if (nextBtn) {
+ nextBtn.addEventListener("click", () => {
+ const majority = Math.ceil(maxRounds / 2);
+ const matchDone = round >= maxRounds
+ || scores.p1 >= majority
+ || scores.p2 >= majority;
+ if (matchDone) {
+ renderFinal();
+ showScreen("ttt-final");
+ } else {
+ round++;
+ newRound();
+ }
+ });
+ }
+
+ // โโ Round management โโ
+ function newRound() {
+ board = Array(9).fill(null);
+ current = "X";
+ gameOver = false;
+
+ // Reset cells
+ document.querySelectorAll(".ttt-cell").forEach(c => {
+ c.textContent = "";
+ c.className = "ttt-cell";
+ c.disabled = false;
+ });
- const cells = document.querySelectorAll('.cell');
+ // Hide result overlay
+ const overlay = document.getElementById("ttt-result-overlay");
+ if (overlay) overlay.style.display = "none";
- board[index] = currentPlayer;
+ // Clear win line
+ clearWinLine();
- cells[index].innerText = currentPlayer;
+ // Update scoreboard
+ syncScoreboard();
- if (currentPlayer === 'X') {
- cells[index].style.color = '#ff4d4d';
- } else {
- cells[index].style.color = '#4d79ff';
+ // Round label
+ const tag = document.getElementById("ttt-round-tag");
+ if (tag) {
+ tag.textContent = maxRounds === 1
+ ? "Single Round"
+ : `Round ${round} of ${maxRounds}`;
}
- // CHECK WINNER FIRST
- if (checkWinner()) {
-
- document.getElementById('status').innerText =
- `Player ${currentPlayer} Wins!`;
+ refreshTurnBanner();
- gameActive = false;
- return;
+ // If AI goes first (not default, but safe to handle)
+ if (mode === "ai" && current === "O") {
+ lockBoard(true);
+ setTimeout(aiTurn, 600);
}
-
- // DRAW CONDITION
- const isDraw = board.every(cell => cell !== '');
-
- if (isDraw) {
-
- document.getElementById('status').innerText =
- "It's a Draw!";
-
- gameActive = false;
- return;
+ }
+
+ function syncScoreboard() {
+ const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
+ set("ttt-sn1", p1);
+ set("ttt-sn2", p2);
+ set("ttt-sv1", scores.p1);
+ set("ttt-sv2", scores.p2);
+ set("ttt-svd", scores.draws);
+ }
+
+ function refreshTurnBanner() {
+ const name = current === "X" ? p1 : p2;
+ const sym = current === "X" ? "X" : "O";
+ const symEl = document.getElementById("ttt-turn-sym");
+ const nameEl = document.getElementById("ttt-turn-name");
+ const banner = document.getElementById("ttt-turn-banner");
+ if (symEl) symEl.textContent = sym;
+ if (nameEl) nameEl.textContent = name;
+ if (banner) {
+ banner.classList.toggle("ttt-turn-banner--x", current === "X");
+ banner.classList.toggle("ttt-turn-banner--o", current === "O");
}
+ }
- // SWITCH PLAYER
- currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
-
- document.getElementById('status').innerText =
- `Player ${currentPlayer}'s Turn`;
-}
-
-
-function checkWinner() {
-
- return winningPatterns.some(pattern => {
-
- return pattern.every(index => {
- return board[index] === currentPlayer;
- });
+ // โโ Cell clicks โโ
+ document.querySelectorAll(".ttt-cell").forEach(cell => {
+ cell.addEventListener("click", () => {
+ const i = parseInt(cell.dataset.i);
+ if (gameOver || board[i]) return;
+ if (mode === "ai" && current === "O") return; // AI's turn
+ placeMove(i, current);
+ afterMove();
});
-}
+ });
+
+ function placeMove(i, sym) {
+ board[i] = sym;
+ const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`);
+ if (!cell) return;
+ cell.textContent = sym;
+ cell.classList.add(sym === "X" ? "ttt-cell--x" : "ttt-cell--o");
+ cell.disabled = true;
+ }
+
+ function afterMove() {
+ const win = getWinner(board);
+ if (win) { endRound(win); return; }
+ if (board.every(c=>c)){ endRound(null); return; }
+
+ current = current === "X" ? "O" : "X";
+ refreshTurnBanner();
+
+ if (mode === "ai" && current === "O") {
+ lockBoard(true);
+ setTimeout(aiTurn, 480 + Math.random()*200);
+ }
+ }
-function resetGame() {
+ function lockBoard(on) {
+ document.querySelectorAll(".ttt-cell").forEach(c => {
+ if (!board[parseInt(c.dataset.i)]) c.disabled = on;
+ });
+ }
+
+ // โโ Win detection โโ
+ function getWinner(b) {
+ for (let idx=0; idx
{
+ const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`);
+ if (cell) cell.classList.add("ttt-cell--win");
+ });
+ // Draw SVG win line
+ drawWinLine(win.coordIdx);
+
+ const winnerName = win.sym === "X" ? p1 : p2;
+ if (win.sym === "X") scores.p1++; else scores.p2++;
+ syncScoreboard();
+
+ setResult("๐", `${winnerName} wins this round!`);
+ } else {
+ scores.draws++;
+ syncScoreboard();
+ setResult("๐ค", "It's a draw!");
+ }
- board = [
- '', '', '',
- '', '', '',
- '', '', ''
- ];
+ // Update Next button label
+ setTimeout(() => {
+ const majority = Math.ceil(maxRounds / 2);
+ const matchDone = round >= maxRounds
+ || scores.p1 >= majority
+ || scores.p2 >= majority;
+ const nextBtn = document.getElementById("ttt-next");
+ if (nextBtn) nextBtn.textContent = matchDone ? "See Results โ" : "Next Round โ";
+
+ const overlay = document.getElementById("ttt-result-overlay");
+ if (overlay) overlay.style.display = "flex";
+ }, 600);
+ }
+
+ function setResult(emoji, text) {
+ const e = document.getElementById("ttt-res-emoji");
+ const t = document.getElementById("ttt-res-text");
+ if (e) e.textContent = emoji;
+ if (t) t.textContent = text;
+ }
+
+ // โโ Win-line SVG โโ
+ // Grid cells are (col, row) 0-indexed; centre of cell = col+0.5, row+0.5
+ function drawWinLine(comboIdx) {
+ const line = document.getElementById("ttt-win-line");
+ const svg = document.getElementById("ttt-win-svg");
+ if (!line || !svg) return;
+
+ const [[c1,r1],[c2,r2]] = WIN_COORDS[comboIdx];
+ line.setAttribute("x1", c1 + 0.5);
+ line.setAttribute("y1", r1 + 0.5);
+ line.setAttribute("x2", c2 + 0.5);
+ line.setAttribute("y2", r2 + 0.5);
+ line.setAttribute("opacity", "1");
+ svg.classList.add("ttt-win-svg--visible");
+}
- currentPlayer = 'X';
+ function clearWinLine() {
+ const line = document.getElementById("ttt-win-line");
+ const svg = document.getElementById("ttt-win-svg");
+ if (line) line.setAttribute("opacity","0");
+ if (svg) svg.classList.remove("ttt-win-svg--visible");
+ }
+
+ // โโ Final screen โโ
+ function renderFinal() {
+ const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; };
+ set("ttt-fp1", p1);
+ set("ttt-fp2", p2);
+ set("ttt-fp1s", scores.p1);
+ set("ttt-fp2s", scores.p2);
+ set("ttt-final-draws", `${scores.draws} draw${scores.draws!==1?"s":""}`);
+
+ let title;
+ if (scores.p1 > scores.p2) title = `๐ ${p1} wins the match!`;
+ else if (scores.p2 > scores.p1) title = `๐ ${p2} wins the match!`;
+ else title = "๐ค The match is tied!";
+ set("ttt-final-title", title);
+ }
+
+ // โโ AI engines โโ
+ function freeCells(b) {
+ return b.reduce((acc,v,i) => { if(!v) acc.push(i); return acc; }, []);
+ }
+
+ function checkWinFor(b, sym) {
+ return WINS.some(([a,x,c]) => b[a]===sym && b[x]===sym && b[c]===sym);
+ }
+
+ function minimax(b, isMax, alpha, beta, depth) {
+ if (checkWinFor(b,"O")) return 10 - depth;
+ if (checkWinFor(b,"X")) return depth - 10;
+ if (b.every(c=>c)) return 0;
+
+ const moves = freeCells(b);
+ if (isMax) {
+ let best = -Infinity;
+ for (const m of moves) {
+ b[m] = "O";
+ best = Math.max(best, minimax(b, false, alpha, beta, depth+1));
+ b[m] = null;
+ alpha = Math.max(alpha, best);
+ if (beta <= alpha) break;
+ }
+ return best;
+ } else {
+ let best = Infinity;
+ for (const m of moves) {
+ b[m] = "X";
+ best = Math.min(best, minimax(b, true, alpha, beta, depth+1));
+ b[m] = null;
+ beta = Math.min(beta, best);
+ if (beta <= alpha) break;
+ }
+ return best;
+ }
+ }
- gameActive = true;
+ function chooseMove(b, diff) {
+ const moves = freeCells(b);
+ if (!moves.length) return null;
- const cells = document.querySelectorAll('.cell');
+ // Easy โ random
+ if (diff === "easy") return moves[Math.floor(Math.random()*moves.length)];
- cells.forEach(cell => {
- cell.textContent = '';
- });
+ // Medium โ win โ block โ center/corners
+ if (diff === "medium") {
+ for (const m of moves) { b[m]="O"; if(checkWinFor(b,"O")){b[m]=null;return m;} b[m]=null; }
+ for (const m of moves) { b[m]="X"; if(checkWinFor(b,"X")){b[m]=null;return m;} b[m]=null; }
+ for (const p of [4,0,2,6,8,1,3,5,7]) { if(!b[p]) return p; }
+ return moves[0];
+ }
- document.getElementById('status').textContent =
- "Player X's Turn";
-}
+ // Hard โ minimax
+ let bestScore=-Infinity, bestMove=moves[0];
+ for (const m of moves) {
+ b[m]="O";
+ const s = minimax(b, false, -Infinity, Infinity, 0);
+ b[m]=null;
+ if (s > bestScore) { bestScore=s; bestMove=m; }
+ }
+ return bestMove;
+ }
+
+ function aiTurn() {
+ if (gameOver) return;
+ const move = chooseMove([...board], difficulty); // pass copy so minimax doesn't corrupt state
+ lockBoard(false);
+ if (move !== null) placeMove(move, "O");
+ afterMove();
+ }
+
+} // end initTicTacToe
+// ================================
function getProductivePetHTML() {
return `
@@ -3276,7 +3631,8 @@ function initializeProject(projectName) {
'simon-says': 'initSimonSays',
'2048-game': 'init2048Game',
'color-palette': 'initColorPalette',
- 'math-quiz': 'initMathQuiz'
+ 'math-quiz': 'initMathQuiz',
+ 'caesar-cipher': 'initCaesarCipher'
};
const initializerName = initializers[projectName];
diff --git a/web-app/js/projects/ceasar-ciphar.js b/web-app/js/projects/ceasar-ciphar.js
new file mode 100644
index 0000000..21e449a
--- /dev/null
+++ b/web-app/js/projects/ceasar-ciphar.js
@@ -0,0 +1,412 @@
+function getCaesarCipherHTML() {
+ return `
+
+
๐ Caesar Cipher Encoder & Decoder
+
Encrypt and decrypt messages using Caesar Cipher
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Output:
+
+
Your result will appear here...
+
+
+
+
+
+
๐ก How the shift works:
+
+
+
+
+
+
+ `;
+}
+
+
+function initCaesarCipher() {
+ const textBox = document.getElementById('cipherInput');
+ const goBtn = document.getElementById('cipherBtn');
+ const clearBtn = document.getElementById('clearBtn');
+ const copyBtn = document.getElementById('copyBtn');
+ const outputBox = document.getElementById('cipherOutput');
+ const shiftNumber = document.getElementById('shiftInput');
+ const shiftSlide = document.getElementById('shiftSlider');
+ const encryptRadio = document.getElementById('modeEncrypt');
+ const decryptRadio = document.getElementById('modeDecrypt');
+ const theLabel = document.getElementById('inputLabel');
+ const previewDiv = document.getElementById('shiftPreview');
+
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+ function encryptText(text, shift) {
+ let result = '';
+
+ for (const ch of text) {
+ if (ch >= 'A' && ch <= 'Z') {
+ result += String.fromCharCode(((ch.charCodeAt(0) - 65 + shift) % 26 + 26) % 26 + 65);
+ } else if (ch >= 'a' && ch <= 'z') {
+ result += String.fromCharCode(((ch.charCodeAt(0) - 97 + shift) % 26 + 26) % 26 + 97);
+ } else {
+ result += ch;
+ }
+ }
+
+ return result;
+ }
+
+ function decryptText(text, shift) {
+ return encryptText(text, -shift);
+ }
+
+ function buildAlphabetRow(label, getChar, className) {
+ const row = document.createElement('div');
+ row.className = 'alphabet-row';
+
+ const labelEl = document.createElement('span');
+ labelEl.className = 'row-label';
+ labelEl.textContent = label;
+ row.appendChild(labelEl);
+
+ for (let i = 0; i < 26; i++) {
+ const box = document.createElement('span');
+ box.className = `letter-box ${className}`;
+ box.textContent = getChar(i);
+ row.appendChild(box);
+ }
+
+ return row;
+ }
+
+ function showPreview(shift) {
+ previewDiv.innerHTML = '';
+ previewDiv.appendChild(buildAlphabetRow('Original:', i => ALPHABET[i], 'plain'));
+ previewDiv.appendChild(buildAlphabetRow(`Shift +${shift}:`, i => ALPHABET[(i + shift) % 26], 'cipher'));
+ }
+
+ function setMode(isEncrypt) {
+ theLabel.textContent = isEncrypt ? '๐ Enter Text to Encrypt:' : '๐จ Enter Text to Decrypt:';
+ textBox.placeholder = isEncrypt ? 'Type your message here...' : 'Paste the encrypted text here...';
+ textBox.value = '';
+ outputBox.innerHTML = '
Your result will appear here...
';
+ copyBtn.style.display = 'none';
+ goBtn.textContent = isEncrypt ? '๐ Encrypt Message' : '๐ Decrypt Message';
+ }
+
+ function syncSlider(val) {
+ const clamped = Math.min(25, Math.max(1, val));
+ shiftNumber.value = clamped;
+ shiftSlide.value = clamped;
+ showPreview(clamped);
+ }
+
+ function showError(msg) {
+ outputBox.innerHTML = `
โ ${msg}
`;
+ copyBtn.style.display = 'none';
+ }
+
+ function showResult(text) {
+ outputBox.innerHTML = '';
+ const p = document.createElement('p');
+ p.className = 'result-text';
+ p.textContent = text;
+ outputBox.appendChild(p);
+ copyBtn.style.display = 'inline-block';
+ copyBtn.dataset.result = text;
+ }
+
+ encryptRadio.addEventListener('change', () => setMode(true));
+ decryptRadio.addEventListener('change', () => setMode(false));
+
+ shiftSlide.addEventListener('input', () => syncSlider(parseInt(shiftSlide.value)));
+ shiftNumber.addEventListener('input', () => syncSlider(parseInt(shiftNumber.value)));
+
+ goBtn.addEventListener('click', () => {
+ const text = textBox.value;
+ const shift = parseInt(shiftNumber.value);
+
+ if (!text.trim()) return showError('Please enter some text first!');
+ if (isNaN(shift) || shift < 1 || shift > 25) return showError('Please enter a shift value between 1 and 25!');
+
+ const result = encryptRadio.checked ? encryptText(text, shift) : decryptText(text, shift);
+ showResult(result);
+ });
+
+ clearBtn.addEventListener('click', () => {
+ textBox.value = '';
+ outputBox.innerHTML = '
Your result will appear here...
';
+ copyBtn.style.display = 'none';
+ });
+
+ copyBtn.addEventListener('click', () => {
+ navigator.clipboard.writeText(copyBtn.dataset.result).then(() => {
+ const original = copyBtn.textContent;
+ copyBtn.textContent = 'โ
Copied!';
+ setTimeout(() => copyBtn.textContent = original, 1500);
+ });
+ });
+
+ showPreview(3);
+}
\ No newline at end of file
diff --git a/web-app/js/projects/tic-tac-toe.js b/web-app/js/projects/tic-tac-toe.js
index c86f237..1f26994 100644
--- a/web-app/js/projects/tic-tac-toe.js
+++ b/web-app/js/projects/tic-tac-toe.js
@@ -1,173 +1,538 @@
+// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+// ๐ฎ Tic Tac Toe โ web-app/js/projects/tic-tac-toe.js
+// Category : games
+// Features : 2-Player | vs AI (Easy/Medium/Hard) |
+// Score tracking | Best-of-1/3/5 rounds
+// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+// โโ 1. HTML Template โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function getTicTacToeHTML() {
- return `
-
-
๐งฉ Tic-Tac-Toe
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Player X's turn
-
-
-
-
- `;
+ return `
+
+
+
+
+
+ X
+ ยท
+ O
+
+
Tic Tac Toe
+
Two players or vs AI โ classic strategy game!
+
+
+
Game Mode
+
+
+
+
+
+
+
+
Difficulty
+
+
+
+
+
+
+
+
+
Rounds
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Round 1 of 3
+
+
+
+ โ
+ Player 1's turn
+
+
+
+
+
+ ${[0,1,2,3,4,5,6,7,8].map(i =>
+ ``
+ ).join('')}
+
+
+
+
+
+
+
+
+
๐
+
Player 1 wins!
+
+
+
+
+
+
+
+
+
+
๐
+
Player 1 Wins!
+
+ P1
+ 0
+ โ
+ 0
+ P2
+
+
0 draws
+
+
+
+
+
+
+
+`;
}
+// โโ 2. Init Function โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function initTicTacToe() {
- const cells = document.querySelectorAll('.cell');
- const statusText = document.getElementById('ticTacToeStatus');
- const restartBtn = document.getElementById('restartTicTacToe');
- const twoPlayerBtn = document.getElementById('twoPlayerMode');
- const computerBtn = document.getElementById('computerMode');
- let vsComputer = false;
- let currentPlayer = 'X';
- let board = ['', '', '', '', '', '', '', '', ''];
- let gameActive = true;
-
-
-
- const pvpBtn = document.getElementById('pvpMode');
- const aiBtn = document.getElementById('aiMode');
-
- pvpBtn.addEventListener('click', () => {
- vsComputer = false;
-
- pvpBtn.classList.add('active-mode');
- aiBtn.classList.remove('active-mode');
-
- resetGame();
- statusText.textContent = "2 Player Mode";
+ // Win combos
+ const WINS = [
+ [0,1,2],[3,4,5],[6,7,8], // rows
+ [0,3,6],[1,4,7],[2,5,8], // cols
+ [0,4,8],[2,4,6] // diagonals
+ ];
+
+ // Win-line centre coordinates (column, row) in 0-2 grid space
+ const WIN_COORDS = [
+ [[0,0],[2,0]], [[0,1],[2,1]], [[0,2],[2,2]], // rows
+ [[0,0],[0,2]], [[1,0],[1,2]], [[2,0],[2,2]], // cols
+ [[0,0],[2,2]], [[2,0],[0,2]] // diagonals
+ ];
+
+ // โโ State โโ
+ let mode = "2p";
+ let difficulty = "easy";
+ let maxRounds = 3;
+ let p1 = "Player 1";
+ let p2 = "Player 2";
+ let scores = { p1:0, p2:0, draws:0 };
+ let board = [];
+ let current = "X"; // "X" | "O"
+ let round = 1;
+ let gameOver = false;
+
+ // โโ Helpers โโ
+ function qs(sel, ctx) { return (ctx||document).querySelector(sel); }
+
+ // Show one of the three screens
+ function showScreen(id) {
+ ["ttt-setup","ttt-game","ttt-final"].forEach(s => {
+ const el = document.getElementById(s);
+ if (el) {
+ el.classList.toggle("ttt-screen--active", s === id);
+ }
});
-
- aiBtn.addEventListener('click', () => {
- vsComputer = true;
-
- aiBtn.classList.add('active-mode');
- pvpBtn.classList.remove('active-mode');
-
- resetGame();
- statusText.textContent = "Playing vs Computer";
+ }
+
+ // Pill-toggle group helper
+ function initPillGroup(groupId, onChange) {
+ const grp = document.getElementById(groupId);
+ if (!grp) return;
+ grp.querySelectorAll(".ttt-pill").forEach(btn => {
+ btn.addEventListener("click", () => {
+ grp.querySelectorAll(".ttt-pill").forEach(b => b.classList.remove("ttt-pill--on"));
+ btn.classList.add("ttt-pill--on");
+ onChange(btn.dataset.val);
+ });
});
-
- const winningCombinations = [
- [0,1,2],
- [3,4,5],
- [6,7,8],
- [0,3,6],
- [1,4,7],
- [2,5,8],
- [0,4,8],
- [2,4,6]
- ];
-
- function checkWinner() {
- for (let combo of winningCombinations) {
- const [a, b, c] = combo;
-
- if (
- board[a] &&
- board[a] === board[b] &&
- board[a] === board[c]
- ) {
- statusText.textContent = `๐ Player ${board[a]} wins!`;
- gameActive = false;
- return;
- }
- }
-
- if (!board.includes('')) {
- statusText.textContent = "๐ค It's a draw!";
- gameActive = false;
- }
+ }
+
+ // โโ Wire up Setup โโ
+ initPillGroup("ttt-mode-group", val => {
+ mode = val;
+ const diffGroup = document.getElementById("ttt-diff-group");
+ const p2box = document.getElementById("ttt-p2-box");
+ const p2inp = document.getElementById("ttt-p2");
+ if (val === "ai") {
+ diffGroup.style.display = "block";
+ p2box.classList.add("ttt-dimmed");
+ p2inp.disabled = true;
+ p2inp.placeholder = "Computer ๐ค";
+ } else {
+ diffGroup.style.display = "none";
+ p2box.classList.remove("ttt-dimmed");
+ p2inp.disabled = false;
+ p2inp.placeholder = "Player 2";
}
+ });
+
+ initPillGroup("ttt-diff-pills", val => { difficulty = val; });
+ initPillGroup("ttt-rounds-group", val => { maxRounds = parseInt(val); });
+
+ // Start button
+ const startBtn = document.getElementById("ttt-start");
+ if (startBtn) {
+ startBtn.addEventListener("click", () => {
+ p1 = (document.getElementById("ttt-p1").value.trim()) || "Player 1";
+ p2 = mode === "ai"
+ ? "Computer ๐ค"
+ : ((document.getElementById("ttt-p2").value.trim()) || "Player 2");
+ scores = { p1:0, p2:0, draws:0 };
+ round = 1;
+ newRound();
+ showScreen("ttt-game");
+ });
+ }
+
+ // Back / Menu buttons
+ const backBtn = document.getElementById("ttt-back");
+ if (backBtn) backBtn.addEventListener("click", () => showScreen("ttt-setup"));
+
+ const menuBtn = document.getElementById("ttt-menu");
+ if (menuBtn) menuBtn.addEventListener("click", () => showScreen("ttt-setup"));
+
+ // Rematch button
+ const rematchBtn = document.getElementById("ttt-rematch");
+ if (rematchBtn) {
+ rematchBtn.addEventListener("click", () => {
+ scores = { p1:0, p2:0, draws:0 };
+ round = 1;
+ newRound();
+ showScreen("ttt-game");
+ });
+ }
+
+ // Next-round button
+ const nextBtn = document.getElementById("ttt-next");
+ if (nextBtn) {
+ nextBtn.addEventListener("click", () => {
+ const majority = Math.ceil(maxRounds / 2);
+ const matchDone = round >= maxRounds
+ || scores.p1 >= majority
+ || scores.p2 >= majority;
+ if (matchDone) {
+ renderFinal();
+ showScreen("ttt-final");
+ } else {
+ round++;
+ newRound();
+ }
+ });
+ }
+
+ // โโ Round management โโ
+ function newRound() {
+ board = Array(9).fill(null);
+ current = "X";
+ gameOver = false;
+
+ // Reset cells
+ document.querySelectorAll(".ttt-cell").forEach(c => {
+ c.textContent = "";
+ c.className = "ttt-cell";
+ c.disabled = false;
+ });
- cells.forEach(cell => {
- cell.addEventListener('click', () => {
- const index = cell.dataset.cell;
+ // Hide result overlay
+ const overlay = document.getElementById("ttt-result-overlay");
+ if (overlay) overlay.style.display = "none";
- if (board[index] || !gameActive) return;
+ // Clear win line
+ clearWinLine();
- board[index] = currentPlayer;
- cell.textContent = currentPlayer;
+ // Update scoreboard
+ syncScoreboard();
- checkWinner();
+ // Round label
+ const tag = document.getElementById("ttt-round-tag");
+ if (tag) {
+ tag.textContent = maxRounds === 1
+ ? "Single Round"
+ : `Round ${round} of ${maxRounds}`;
+ }
- if (vsComputer && gameActive && currentPlayer === 'X') {
- currentPlayer = 'O';
- statusText.textContent = "Computer's turn";
+ refreshTurnBanner();
- setTimeout(() => {
- computerMove();
- }, 500);
+ // If AI goes first (not default, but safe to handle)
+ if (mode === "ai" && current === "O") {
+ lockBoard(true);
+ setTimeout(aiTurn, 600);
+ }
+ }
+
+ function syncScoreboard() {
+ const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
+ set("ttt-sn1", p1);
+ set("ttt-sn2", p2);
+ set("ttt-sv1", scores.p1);
+ set("ttt-sv2", scores.p2);
+ set("ttt-svd", scores.draws);
+ }
+
+ function refreshTurnBanner() {
+ const name = current === "X" ? p1 : p2;
+ const sym = current === "X" ? "X" : "O";
+ const symEl = document.getElementById("ttt-turn-sym");
+ const nameEl = document.getElementById("ttt-turn-name");
+ const banner = document.getElementById("ttt-turn-banner");
+ if (symEl) symEl.textContent = sym;
+ if (nameEl) nameEl.textContent = name;
+ if (banner) {
+ banner.classList.toggle("ttt-turn-banner--x", current === "X");
+ banner.classList.toggle("ttt-turn-banner--o", current === "O");
+ }
+ }
- return;
- }
+ // โโ Cell clicks โโ
+ document.querySelectorAll(".ttt-cell").forEach(cell => {
+ cell.addEventListener("click", () => {
+ const i = parseInt(cell.dataset.i);
+ if (gameOver || board[i]) return;
+ if (mode === "ai" && current === "O") return; // AI's turn
- if (gameActive) {
- currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
- statusText.textContent = `Player ${currentPlayer}'s turn`;
- }
- });
+ placeMove(i, current);
+ afterMove();
});
-
- function resetGame() {
- board = ['', '', '', '', '', '', '', '', ''];
- gameActive = true;
- currentPlayer = 'X';
-
- cells.forEach(cell => {
- cell.textContent = '';
- });
-
- statusText.textContent = "Player X's turn";
+ });
+
+ function placeMove(i, sym) {
+ board[i] = sym;
+ const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`);
+ if (!cell) return;
+ cell.textContent = sym;
+ cell.classList.add(sym === "X" ? "ttt-cell--x" : "ttt-cell--o");
+ cell.disabled = true;
+ }
+
+ function afterMove() {
+ const win = getWinner(board);
+ if (win) { endRound(win); return; }
+ if (board.every(c=>c)){ endRound(null); return; }
+
+ current = current === "X" ? "O" : "X";
+ refreshTurnBanner();
+
+ if (mode === "ai" && current === "O") {
+ lockBoard(true);
+ setTimeout(aiTurn, 480 + Math.random()*200);
}
+ }
- function computerMove() {
- let emptyCells = [];
-
- board.forEach((cell, index) => {
- if (cell === '') {
- emptyCells.push(index);
- }
- });
-
- if (emptyCells.length === 0) return;
+ function lockBoard(on) {
+ document.querySelectorAll(".ttt-cell").forEach(c => {
+ if (!board[parseInt(c.dataset.i)]) c.disabled = on;
+ });
+ }
+
+ // โโ Win detection โโ
+ function getWinner(b) {
+ for (let idx=0; idx
{
+ const cell = document.querySelector(`.ttt-cell[data-i="${i}"]`);
+ if (cell) cell.classList.add("ttt-cell--win");
+ });
+ // Draw SVG win line
+ drawWinLine(win.coordIdx);
+
+ const winnerName = win.sym === "X" ? p1 : p2;
+ if (win.sym === "X") scores.p1++; else scores.p2++;
+ syncScoreboard();
+
+ setResult("๐", `${winnerName} wins this round!`);
+ } else {
+ scores.draws++;
+ syncScoreboard();
+ setResult("๐ค", "It's a draw!");
+ }
- const randomIndex =
- emptyCells[Math.floor(Math.random() * emptyCells.length)];
+ // Update Next button label
+ setTimeout(() => {
+ const majority = Math.ceil(maxRounds / 2);
+ const matchDone = round >= maxRounds
+ || scores.p1 >= majority
+ || scores.p2 >= majority;
+ const nextBtn = document.getElementById("ttt-next");
+ if (nextBtn) nextBtn.textContent = matchDone ? "See Results โ" : "Next Round โ";
+
+ const overlay = document.getElementById("ttt-result-overlay");
+ if (overlay) overlay.style.display = "flex";
+ }, 600);
+ }
+
+ function setResult(emoji, text) {
+ const e = document.getElementById("ttt-res-emoji");
+ const t = document.getElementById("ttt-res-text");
+ if (e) e.textContent = emoji;
+ if (t) t.textContent = text;
+ }
+
+ // โโ Win-line SVG โโ
+ // Grid cells are (col, row) 0-indexed; centre of cell = col+0.5, row+0.5
+ function drawWinLine(comboIdx) {
+ const line = document.getElementById("ttt-win-line");
+ const svg = document.getElementById("ttt-win-svg");
+ if (!line || !svg) return;
+
+ const [[c1,r1],[c2,r2]] = WIN_COORDS[comboIdx];
+ line.setAttribute("x1", c1 + 0.5);
+ line.setAttribute("y1", r1 + 0.5);
+ line.setAttribute("x2", c2 + 0.5);
+ line.setAttribute("y2", r2 + 0.5);
+ line.setAttribute("opacity", "1");
+ svg.classList.add("ttt-win-svg--visible");
+}
+ function clearWinLine() {
+ const line = document.getElementById("ttt-win-line");
+ const svg = document.getElementById("ttt-win-svg");
+ if (line) line.setAttribute("opacity","0");
+ if (svg) svg.classList.remove("ttt-win-svg--visible");
+ }
+
+ // โโ Final screen โโ
+ function renderFinal() {
+ const set = (id,v) => { const el=document.getElementById(id); if(el) el.textContent=v; };
+ set("ttt-fp1", p1);
+ set("ttt-fp2", p2);
+ set("ttt-fp1s", scores.p1);
+ set("ttt-fp2s", scores.p2);
+ set("ttt-final-draws", `${scores.draws} draw${scores.draws!==1?"s":""}`);
+
+ let title;
+ if (scores.p1 > scores.p2) title = `๐ ${p1} wins the match!`;
+ else if (scores.p2 > scores.p1) title = `๐ ${p2} wins the match!`;
+ else title = "๐ค The match is tied!";
+ set("ttt-final-title", title);
+ }
+
+ // โโ AI engines โโ
+ function freeCells(b) {
+ return b.reduce((acc,v,i) => { if(!v) acc.push(i); return acc; }, []);
+ }
+
+ function checkWinFor(b, sym) {
+ return WINS.some(([a,x,c]) => b[a]===sym && b[x]===sym && b[c]===sym);
+ }
+
+ function minimax(b, isMax, alpha, beta, depth) {
+ if (checkWinFor(b,"O")) return 10 - depth;
+ if (checkWinFor(b,"X")) return depth - 10;
+ if (b.every(c=>c)) return 0;
+
+ const moves = freeCells(b);
+ if (isMax) {
+ let best = -Infinity;
+ for (const m of moves) {
+ b[m] = "O";
+ best = Math.max(best, minimax(b, false, alpha, beta, depth+1));
+ b[m] = null;
+ alpha = Math.max(alpha, best);
+ if (beta <= alpha) break;
+ }
+ return best;
+ } else {
+ let best = Infinity;
+ for (const m of moves) {
+ b[m] = "X";
+ best = Math.min(best, minimax(b, true, alpha, beta, depth+1));
+ b[m] = null;
+ beta = Math.min(beta, best);
+ if (beta <= alpha) break;
+ }
+ return best;
+ }
+ }
- board[randomIndex] = 'O';
- cells[randomIndex].textContent = 'O';
+ function chooseMove(b, diff) {
+ const moves = freeCells(b);
+ if (!moves.length) return null;
- checkWinner();
+ // Easy โ random
+ if (diff === "easy") return moves[Math.floor(Math.random()*moves.length)];
- if (gameActive) {
- currentPlayer = 'X';
- statusText.textContent = "Player X's turn";
- }
+ // Medium โ win โ block โ center/corners
+ if (diff === "medium") {
+ for (const m of moves) { b[m]="O"; if(checkWinFor(b,"O")){b[m]=null;return m;} b[m]=null; }
+ for (const m of moves) { b[m]="X"; if(checkWinFor(b,"X")){b[m]=null;return m;} b[m]=null; }
+ for (const p of [4,0,2,6,8,1,3,5,7]) { if(!b[p]) return p; }
+ return moves[0];
}
- restartBtn.addEventListener('click', resetGame);
-}
\ No newline at end of file
+ // Hard โ minimax
+ let bestScore=-Infinity, bestMove=moves[0];
+ for (const m of moves) {
+ b[m]="O";
+ const s = minimax(b, false, -Infinity, Infinity, 0);
+ b[m]=null;
+ if (s > bestScore) { bestScore=s; bestMove=m; }
+ }
+ return bestMove;
+ }
+
+ function aiTurn() {
+ if (gameOver) return;
+ const move = chooseMove([...board], difficulty); // pass copy so minimax doesn't corrupt state
+ lockBoard(false);
+ if (move !== null) placeMove(move, "O");
+ afterMove();
+ }
+
+} // end initTicTacToe
\ No newline at end of file
diff --git a/web-app/math.html b/web-app/math.html
index 46a91a0..71c5785 100644
--- a/web-app/math.html
+++ b/web-app/math.html
@@ -91,44 +91,103 @@
-
+
๐ Skip to main content
-
-