| Server IP : 167.235.67.158 / Your IP : 216.73.216.95 Web Server : Apache System : Linux ubuntu-8gb-nbg1-1 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 User : root ( 0) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bc.the-word.com/ |
Upload File : |
/**
* 2026 Challenge Calendar App
* A space-themed interactive calendar with 52 weekly challenges
*/
// ============================================
// PIXEL ART LAYOUT FOR "2026"
// ============================================
// Grid is 18 columns x 7 rows
// 1 = pixel on (part of the number), 0 = pixel off
// Total "on" pixels = 52 (one per week)
const PIXEL_ART_2026 = [
// Row 0: "2" "0" "2" "6"
// (3px) (2px) (3px) (3px) = 11
[1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1],
// Row 1: (1px) (2px) (1px) (1px) = 5
[0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
// Row 2: (1px) (2px) (1px) (1px) = 5
[0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
// Row 3: (1px) (2px) (1px) (3px) = 7
[0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0],
// Row 4: (1px) (2px) (1px) (2px) = 6
[1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
// Row 5: (1px) (2px) (1px) (2px) = 6
[1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
// Row 6: (4px) (2px) (4px) (2px) = 12
[1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0],
// Digit totals: "2"=12, "0"=14, "2"=12, "6"=14 = 52 pixels!
];
// Define which columns belong to which digit (for sequential week numbering)
const DIGIT_COLUMNS = {
0: [0, 1, 2, 3], // First "2"
1: [5, 6, 7, 8], // "0"
2: [10, 11, 12, 13], // Second "2"
3: [15, 16, 17, 18] // "6"
};
// ============================================
// APP STATE
// ============================================
let challenges = [];
let openedChallenges = new Set();
let currentZoom = 0.8; // Start slightly zoomed out to fit "2026"
let panX = 0;
let panY = 0;
let isDragging = false;
let lastTouchDistance = 0;
let lastPanX = 0;
let lastPanY = 0;
let startX = 0;
let startY = 0;
// Audio context for sounds
let audioContext = null;
// ============================================
// INITIALIZATION
// ============================================
document.addEventListener('DOMContentLoaded', async () => {
// Register Service Worker for PWA
registerServiceWorker();
// Load saved state
loadOpenedChallenges();
// Load challenges from JSON
await loadChallenges();
// Initialize starfield
initStarfield();
// Generate pixel grid
generatePixelGrid();
// Setup event listeners
setupEventListeners();
// Initialize audio context on first user interaction
document.addEventListener('touchstart', initAudio, { once: true });
document.addEventListener('click', initAudio, { once: true });
});
// ============================================
// SERVICE WORKER REGISTRATION
// ============================================
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('service-worker.js');
console.log('Service Worker registered:', registration.scope);
} catch (error) {
console.error('Service Worker registration failed:', error);
}
}
}
// ============================================
// DATA LOADING
// ============================================
async function loadChallenges() {
try {
const response = await fetch('challenges.json');
const data = await response.json();
challenges = data.challenges;
} catch (error) {
console.error('Failed to load challenges:', error);
// Generate placeholder challenges if file doesn't exist
challenges = generatePlaceholderChallenges();
}
}
function generatePlaceholderChallenges() {
const placeholders = [];
const startDate = new Date('2026-01-01');
for (let i = 1; i <= 52; i++) {
const unlockDate = new Date(startDate);
unlockDate.setDate(startDate.getDate() + (i - 1) * 7);
placeholders.push({
week: i,
unlockDate: unlockDate.toISOString().split('T')[0],
title: `Week ${i}`,
challenge: `Challenge for week ${i} - Coming soon!`
});
}
return placeholders;
}
function loadOpenedChallenges() {
try {
const saved = localStorage.getItem('openedChallenges');
if (saved) {
openedChallenges = new Set(JSON.parse(saved));
}
} catch (error) {
console.error('Failed to load opened challenges:', error);
}
}
function saveOpenedChallenges() {
try {
localStorage.setItem('openedChallenges', JSON.stringify([...openedChallenges]));
} catch (error) {
console.error('Failed to save opened challenges:', error);
}
}
// ============================================
// PIXEL GRID GENERATION
// ============================================
function generatePixelGrid() {
const grid = document.getElementById('pixel-grid');
const cols = PIXEL_ART_2026[0].length;
const rows = PIXEL_ART_2026.length;
// Set grid template
grid.style.gridTemplateColumns = `repeat(${cols}, var(--tile-size))`;
grid.style.gridTemplateRows = `repeat(${rows}, var(--tile-size))`;
// Build week number map - assign weeks sequentially per digit
const weekMap = buildWeekMap();
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
if (PIXEL_ART_2026[row][col] === 1) {
const weekNumber = weekMap[`${row},${col}`];
const tile = createTile(weekNumber);
grid.appendChild(tile);
} else {
// Empty space placeholder
const spacer = document.createElement('div');
spacer.className = 'pixel-spacer';
spacer.style.width = 'var(--tile-size)';
spacer.style.height = 'var(--tile-size)';
grid.appendChild(spacer);
}
}
}
// Apply initial zoom
updateTransform();
}
// Build a map of (row,col) -> week number, numbering each digit sequentially
function buildWeekMap() {
const weekMap = {};
let weekNumber = 1;
const rows = PIXEL_ART_2026.length;
// Process each digit in order (0=first "2", 1="0", 2=second "2", 3="6")
for (let digit = 0; digit <= 3; digit++) {
const digitCols = DIGIT_COLUMNS[digit];
// Go through each row, but only for this digit's columns
for (let row = 0; row < rows; row++) {
for (const col of digitCols) {
if (PIXEL_ART_2026[row][col] === 1) {
weekMap[`${row},${col}`] = weekNumber;
weekNumber++;
}
}
}
}
return weekMap;
}
function createTile(weekNumber) {
const tile = document.createElement('div');
tile.className = 'pixel-tile';
tile.dataset.week = weekNumber;
// Add accessibility attributes
tile.setAttribute('role', 'button');
tile.setAttribute('tabindex', '0');
tile.setAttribute('aria-label', `Week ${weekNumber} challenge`);
const challenge = challenges.find(c => c.week === weekNumber);
const unlockDate = challenge ? new Date(challenge.unlockDate) : getDefaultUnlockDate(weekNumber);
const isUnlocked = isWeekUnlocked(unlockDate);
const isOpened = openedChallenges.has(weekNumber);
// Set state class
if (isOpened) {
tile.classList.add('opened');
const weekNum = document.createElement('span');
weekNum.className = 'week-num';
weekNum.textContent = weekNumber;
tile.appendChild(weekNum);
} else if (isUnlocked) {
tile.classList.add('unlocked');
const weekNum = document.createElement('span');
weekNum.className = 'week-num';
weekNum.textContent = weekNumber;
tile.appendChild(weekNum);
} else {
tile.classList.add('locked');
}
// Store unlock date for tooltip
tile.dataset.unlockDate = unlockDate.toISOString();
// Click handler
tile.addEventListener('click', () => handleTileClick(weekNumber));
// Keyboard handler for accessibility
tile.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleTileClick(weekNumber);
}
});
return tile;
}
function getDefaultUnlockDate(weekNumber) {
const startDate = new Date('2026-01-01');
startDate.setDate(startDate.getDate() + (weekNumber - 1) * 7);
return startDate;
}
function isWeekUnlocked(unlockDate) {
// Check for test mode via URL parameter ?test=true
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('test') === 'true') {
return true; // All weeks unlocked in test mode
}
const now = new Date();
return now >= unlockDate;
}
// ============================================
// TILE CLICK HANDLING
// ============================================
function handleTileClick(weekNumber) {
const challenge = challenges.find(c => c.week === weekNumber);
const unlockDate = challenge ? new Date(challenge.unlockDate) : getDefaultUnlockDate(weekNumber);
if (!isWeekUnlocked(unlockDate)) {
showLockedToast(unlockDate);
return;
}
// Mark as opened
openedChallenges.add(weekNumber);
saveOpenedChallenges();
// Update tile appearance
const tile = document.querySelector(`[data-week="${weekNumber}"]`);
if (tile) {
tile.classList.remove('unlocked');
tile.classList.add('opened');
}
// Show cube animation and challenge
showChallenge(weekNumber, challenge);
}
function showLockedToast(unlockDate) {
const toast = document.getElementById('locked-toast');
const message = document.getElementById('locked-message');
const options = { month: 'long', day: 'numeric' };
const dateStr = unlockDate.toLocaleDateString('en-US', options);
message.textContent = `Opens on ${dateStr}`;
toast.classList.remove('hidden');
// Play locked sound
playSound('locked');
// Hide after 2 seconds
setTimeout(() => {
toast.classList.add('hidden');
}, 2000);
}
// ============================================
// CHALLENGE MODAL & CUBE ANIMATION
// ============================================
function showChallenge(weekNumber, challenge) {
const overlay = document.getElementById('modal-overlay');
const cube = document.getElementById('cube');
const cubeContainer = document.getElementById('cube-container');
const challengeContent = document.getElementById('challenge-content');
const cubeWeek = document.getElementById('cube-week');
// Reset state
cube.className = '';
challengeContent.classList.add('hidden');
cubeContainer.style.display = 'block';
// Set week number on cube
cubeWeek.textContent = weekNumber;
// Show overlay
overlay.classList.remove('hidden');
// Play whoosh sound
playSound('whoosh');
// Start cube spinning animation
setTimeout(() => {
cube.classList.add('spinning');
}, 100);
// After spin, open cube and show challenge
setTimeout(() => {
cube.classList.remove('spinning');
cube.classList.add('opening');
playSound('unlock');
// Show confetti
launchConfetti();
}, 1600);
// Show challenge content
setTimeout(() => {
cubeContainer.style.display = 'none';
challengeContent.classList.remove('hidden');
// Populate challenge content
document.getElementById('challenge-week-num').textContent = weekNumber;
const unlockDate = challenge ? new Date(challenge.unlockDate) : getDefaultUnlockDate(weekNumber);
const options = { month: 'long', day: 'numeric', year: 'numeric' };
document.getElementById('challenge-date').textContent = unlockDate.toLocaleDateString('en-US', options);
document.getElementById('challenge-text').textContent =
challenge ? challenge.challenge : `Challenge ${weekNumber} - Coming soon!`;
}, 2200);
}
function closeChallenge() {
const overlay = document.getElementById('modal-overlay');
overlay.classList.add('hidden');
}
// ============================================
// STARFIELD BACKGROUND
// ============================================
function initStarfield() {
const canvas = document.getElementById('starfield');
const ctx = canvas.getContext('2d');
// Pre-create the nebula gradient
let nebulaGradient = null;
function createNebulaGradient() {
nebulaGradient = ctx.createRadialGradient(
canvas.width * 0.3, canvas.height * 0.4, 0,
canvas.width * 0.3, canvas.height * 0.4, canvas.width * 0.5
);
nebulaGradient.addColorStop(0, 'rgba(124, 58, 237, 0.1)');
nebulaGradient.addColorStop(0.5, 'rgba(59, 130, 246, 0.05)');
nebulaGradient.addColorStop(1, 'transparent');
}
// Set canvas size
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
createNebulaGradient();
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Create stars with twinkling properties - reduced count for performance
const stars = [];
const numStars = 100; // Reduced from 200
// Pre-calculate colors array
const starColors = ['#ffffff', '#e0e7ff', '#c4b5fd', '#a5b4fc', '#93c5fd'];
for (let i = 0; i < numStars; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 1.5 + 0.5,
baseAlpha: Math.random() * 0.5 + 0.3,
alpha: Math.random(),
twinkleSpeed: Math.random() * 0.02 + 0.005,
twinklePhase: Math.random() * Math.PI * 2,
twinkleIntensity: Math.random() * 0.4 + 0.2,
color: starColors[Math.floor(Math.random() * starColors.length)]
});
}
// Throttle animation to ~30fps for better performance
let lastFrameTime = 0;
const frameInterval = 1000 / 30;
// Animation loop
function animate(currentTime) {
requestAnimationFrame(animate);
// Throttle frame rate
if (currentTime - lastFrameTime < frameInterval) return;
lastFrameTime = currentTime;
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw nebula effect (using cached gradient)
ctx.fillStyle = nebulaGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw stars with simple twinkling
for (let i = 0; i < stars.length; i++) {
const star = stars[i];
// Simple sine-wave twinkling
star.twinklePhase += star.twinkleSpeed;
star.alpha = star.baseAlpha + Math.sin(star.twinklePhase) * star.twinkleIntensity;
ctx.globalAlpha = star.alpha;
ctx.fillStyle = star.color;
ctx.beginPath();
ctx.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
requestAnimationFrame(animate);
}
// ============================================
// CONFETTI EFFECT
// ============================================
function launchConfetti() {
const canvas = document.getElementById('confetti-canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const colors = ['#8b5cf6', '#ec4899', '#3b82f6', '#fbbf24', '#10b981', '#06b6d4'];
const numParticles = 80; // Fewer particles for quick burst
// Create particles - burst outward from center
for (let i = 0; i < numParticles; i++) {
const angle = (Math.PI * 2 * i) / numParticles;
const speed = Math.random() * 15 + 10;
particles.push({
x: canvas.width / 2,
y: canvas.height / 2,
vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 5,
vy: Math.sin(angle) * speed - 5,
color: colors[Math.floor(Math.random() * colors.length)],
size: Math.random() * 8 + 4,
rotation: Math.random() * 360,
rotationSpeed: (Math.random() - 0.5) * 15,
gravity: 0.5,
alpha: 1,
shape: Math.random() > 0.5 ? 'square' : 'circle'
});
}
function animateConfetti() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let activeParticles = 0;
particles.forEach(p => {
if (p.alpha <= 0) return;
activeParticles++;
// Update position
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.rotation += p.rotationSpeed;
p.alpha -= 0.04; // Much faster fade for quick burst
p.vx *= 0.98;
// Draw
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rotation * Math.PI / 180);
ctx.globalAlpha = p.alpha;
ctx.fillStyle = p.color;
if (p.shape === 'square') {
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size);
} else {
ctx.beginPath();
ctx.arc(0, 0, p.size / 2, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
});
if (activeParticles > 0) {
requestAnimationFrame(animateConfetti);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
animateConfetti();
}
// ============================================
// SOUND EFFECTS
// ============================================
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playSound(type) {
if (!audioContext) return;
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
switch (type) {
case 'whoosh':
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(200, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(800, audioContext.currentTime + 0.3);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.3);
break;
case 'unlock':
// Play a pleasant chime
const frequencies = [523.25, 659.25, 783.99]; // C5, E5, G5
frequencies.forEach((freq, i) => {
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.connect(gain);
gain.connect(audioContext.destination);
osc.type = 'sine';
osc.frequency.value = freq;
gain.gain.setValueAtTime(0.2, audioContext.currentTime + i * 0.1);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + i * 0.1 + 0.5);
osc.start(audioContext.currentTime + i * 0.1);
osc.stop(audioContext.currentTime + i * 0.1 + 0.5);
});
break;
case 'locked':
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(150, audioContext.currentTime);
oscillator.frequency.setValueAtTime(100, audioContext.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.2);
break;
}
}
// ============================================
// ZOOM & PAN FUNCTIONALITY
// ============================================
function setupEventListeners() {
const viewport = document.getElementById('viewport');
const grid = document.getElementById('pixel-grid');
// Touch events for pinch zoom and pan
viewport.addEventListener('touchstart', handleTouchStart, { passive: false });
viewport.addEventListener('touchmove', handleTouchMove, { passive: false });
viewport.addEventListener('touchend', handleTouchEnd);
// Mouse events for desktop
viewport.addEventListener('mousedown', handleMouseDown);
viewport.addEventListener('mousemove', handleMouseMove);
viewport.addEventListener('mouseup', handleMouseUp);
viewport.addEventListener('mouseleave', handleMouseUp);
viewport.addEventListener('wheel', handleWheel, { passive: false });
// Close challenge button
document.getElementById('close-challenge').addEventListener('click', closeChallenge);
// Close modal on overlay click
document.getElementById('modal-overlay').addEventListener('click', (e) => {
if (e.target.id === 'modal-overlay') {
closeChallenge();
}
});
}
function handleTouchStart(e) {
if (e.touches.length === 2) {
// Pinch zoom start
e.preventDefault();
lastTouchDistance = getTouchDistance(e.touches);
} else if (e.touches.length === 1) {
// Pan start
isDragging = true;
startX = e.touches[0].clientX - panX;
startY = e.touches[0].clientY - panY;
}
}
function handleTouchMove(e) {
if (e.touches.length === 2) {
// Pinch zoom
e.preventDefault();
const currentDistance = getTouchDistance(e.touches);
const delta = currentDistance - lastTouchDistance;
currentZoom = Math.max(0.5, Math.min(3, currentZoom + delta * 0.005));
lastTouchDistance = currentDistance;
updateTransform();
} else if (e.touches.length === 1 && isDragging) {
// Pan
panX = e.touches[0].clientX - startX;
panY = e.touches[0].clientY - startY;
updateTransform();
}
}
function handleTouchEnd() {
isDragging = false;
lastTouchDistance = 0;
}
function getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function handleMouseDown(e) {
if (e.target.classList.contains('pixel-tile')) return;
isDragging = true;
startX = e.clientX - panX;
startY = e.clientY - panY;
}
function handleMouseMove(e) {
if (!isDragging) return;
panX = e.clientX - startX;
panY = e.clientY - startY;
updateTransform();
}
function handleMouseUp() {
isDragging = false;
}
function handleWheel(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
currentZoom = Math.max(0.5, Math.min(3, currentZoom + delta));
updateTransform();
}
function updateTransform() {
const grid = document.getElementById('pixel-grid');
grid.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`;
}
// ============================================
// UTILITY FUNCTIONS
// ============================================
function formatDate(date) {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
return date.toLocaleDateString('en-US', options);
}