Hide easteregg

This commit is contained in:
Simon Stürz 2025-12-11 16:01:23 +01:00
parent 2f2d3baeaa
commit 6bf867d816
2 changed files with 337 additions and 1 deletions

View File

@ -7,6 +7,7 @@ class DashboardApp {
loginError: document.getElementById('loginError'),
username: document.getElementById('username'),
password: document.getElementById('password'),
brandLogo: document.getElementById('brandLogo'),
statusDot: document.getElementById('statusDot'),
connectionStatus: document.getElementById('connectionStatus'),
sessionUsername: document.getElementById('sessionUsername'),
@ -14,6 +15,10 @@ class DashboardApp {
requestTemplate: document.getElementById('requestTemplate'),
responseTemplate: document.getElementById('responseTemplate'),
incomingMessage: document.getElementById('incomingMessage'),
easterEggOverlay: document.getElementById('easterEggOverlay'),
easterEggCanvas: document.getElementById('easterEggCanvas'),
easterEggClose: document.getElementById('easterEggClose'),
easterEggScore: document.getElementById('easterEggScore'),
chargerTableBody: document.getElementById('chargerTableBody'),
chargerEmptyRow: document.getElementById('chargerEmptyRow'),
fetchSessionsButton: document.getElementById('fetchSessionsButton'),
@ -39,6 +44,16 @@ class DashboardApp {
this.cars = new Map();
this.sessions = [];
this.activePanel = null;
this.easterEggClickCount = 0;
this.easterEggClickResetTimer = null;
this.easterEggGame = {
running: false,
frameId: null,
player: { x: 30, y: 30, size: 16, speed: 3.2 },
target: { x: 200, y: 140, size: 10 },
score: 0,
keys: {}
};
this.chargerColumns = [
{ key: 'id', label: 'ID', hidden: true },
{ key: 'name', label: 'Name' },
@ -87,6 +102,25 @@ class DashboardApp {
this.downloadChargingSessionsCsv();
});
}
if (this.elements.brandLogo) {
this.elements.brandLogo.addEventListener('click', () => {
this.handleBrandLogoClick();
});
}
if (this.elements.easterEggClose) {
this.elements.easterEggClose.addEventListener('click', () => {
this.stopEasterEggGame();
});
}
if (this.elements.easterEggOverlay) {
this.elements.easterEggOverlay.addEventListener('click', event => {
if (event.target === this.elements.easterEggOverlay)
this.stopEasterEggGame();
});
}
}
initializePanelNavigation() {
@ -1246,6 +1280,245 @@ class DashboardApp {
return stringValue;
}
handleBrandLogoClick() {
clearTimeout(this.easterEggClickResetTimer);
this.easterEggClickCount += 1;
this.easterEggClickResetTimer = setTimeout(() => {
this.easterEggClickCount = 0;
}, 1200);
if (this.easterEggClickCount >= 10) {
this.easterEggClickCount = 0;
this.startEasterEggGame();
}
}
startEasterEggGame() {
if (!this.elements.easterEggOverlay || !this.elements.easterEggCanvas)
return;
this.elements.easterEggOverlay.classList.remove('hidden');
this.elements.easterEggOverlay.setAttribute('aria-hidden', 'false');
const game = this.easterEggGame;
const canvas = this.elements.easterEggCanvas;
game.running = true;
game.score = 0;
game.keys = {};
game.lastTime = null;
game.player.x = canvas.width * 0.2;
game.player.y = canvas.height * 0.5;
this.spawnEasterEggTarget();
this.updateEasterEggScore();
this.toggleEasterEggListeners(true);
const loop = timestamp => {
if (!game.running)
return;
if (!game.lastTime)
game.lastTime = timestamp;
const delta = Math.min((timestamp - game.lastTime) / 16.67, 3);
game.lastTime = timestamp;
this.updateEasterEggPhysics(delta);
this.drawEasterEggFrame();
game.frameId = window.requestAnimationFrame(loop);
};
game.frameId = window.requestAnimationFrame(loop);
}
stopEasterEggGame() {
const game = this.easterEggGame;
game.running = false;
game.keys = {};
if (game.frameId) {
window.cancelAnimationFrame(game.frameId);
game.frameId = null;
}
if (this.elements.easterEggOverlay) {
this.elements.easterEggOverlay.classList.add('hidden');
this.elements.easterEggOverlay.setAttribute('aria-hidden', 'true');
}
this.toggleEasterEggListeners(false);
}
toggleEasterEggListeners(enable) {
if (!this._easterEggKeyDownHandler) {
this._easterEggKeyDownHandler = event => this.handleEasterEggKey(event, true);
this._easterEggKeyUpHandler = event => this.handleEasterEggKey(event, false);
}
if (enable) {
document.addEventListener('keydown', this._easterEggKeyDownHandler);
document.addEventListener('keyup', this._easterEggKeyUpHandler);
} else {
document.removeEventListener('keydown', this._easterEggKeyDownHandler);
document.removeEventListener('keyup', this._easterEggKeyUpHandler);
}
}
handleEasterEggKey(event, isDown) {
if (!this.easterEggGame.running)
return;
const key = event.key ? event.key.toLowerCase() : '';
const movableKeys = ['arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'w', 'a', 's', 'd'];
if (key === 'escape') {
this.stopEasterEggGame();
return;
}
if (movableKeys.includes(key)) {
event.preventDefault();
this.easterEggGame.keys[key] = isDown;
}
}
updateEasterEggPhysics(delta) {
const canvas = this.elements.easterEggCanvas;
if (!canvas)
return;
const game = this.easterEggGame;
const { player, target } = game;
const input = {
x: (game.keys.arrowright || game.keys.d ? 1 : 0) - (game.keys.arrowleft || game.keys.a ? 1 : 0),
y: (game.keys.arrowdown || game.keys.s ? 1 : 0) - (game.keys.arrowup || game.keys.w ? 1 : 0)
};
if (input.x !== 0 || input.y !== 0) {
const length = Math.hypot(input.x, input.y) || 1;
const speed = player.speed * delta;
player.x += (input.x / length) * speed;
player.y += (input.y / length) * speed;
}
const minX = player.size;
const maxX = canvas.width - player.size;
const minY = player.size;
const maxY = canvas.height - player.size;
player.x = Math.min(Math.max(player.x, minX), maxX);
player.y = Math.min(Math.max(player.y, minY), maxY);
const dx = player.x - target.x;
const dy = player.y - target.y;
const distance = Math.hypot(dx, dy);
if (distance <= player.size + target.size) {
game.score += 1;
player.speed = Math.min(player.speed + 0.15, 7.5);
this.updateEasterEggScore();
this.spawnEasterEggTarget();
}
}
drawEasterEggFrame() {
const canvas = this.elements.easterEggCanvas;
if (!canvas)
return;
const ctx = canvas.getContext('2d');
const game = this.easterEggGame;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#0f1c3d');
gradient.addColorStop(1, '#092037');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
ctx.lineWidth = 1;
for (let x = 20; x < canvas.width; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 20; y < canvas.height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
const { player, target } = game;
ctx.fillStyle = '#f4b400';
ctx.beginPath();
ctx.moveTo(target.x, target.y - target.size);
ctx.lineTo(target.x - target.size * 0.6, target.y + target.size * 0.2);
ctx.lineTo(target.x - target.size * 0.2, target.y + target.size * 0.2);
ctx.lineTo(target.x - target.size, target.y + target.size);
ctx.lineTo(target.x + target.size * 0.2, target.y + target.size * 0.2);
ctx.lineTo(target.x + target.size * 0.6, target.y - target.size * 0.8);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 1.2;
ctx.stroke();
const carWidth = player.size * 2.4;
const carHeight = player.size * 1.4;
const carX = player.x - carWidth / 2;
const carY = player.y - carHeight / 2;
const carGradient = ctx.createLinearGradient(carX, carY, carX + carWidth, carY + carHeight);
carGradient.addColorStop(0, '#e30a18');
carGradient.addColorStop(1, '#f48221');
ctx.fillStyle = carGradient;
ctx.beginPath();
ctx.moveTo(carX + carWidth * 0.15, carY + carHeight);
ctx.lineTo(carX + carWidth * 0.15, carY + carHeight * 0.55);
ctx.lineTo(carX + carWidth * 0.35, carY + carHeight * 0.25);
ctx.lineTo(carX + carWidth * 0.65, carY + carHeight * 0.25);
ctx.lineTo(carX + carWidth * 0.85, carY + carHeight * 0.55);
ctx.lineTo(carX + carWidth * 0.85, carY + carHeight);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.lineWidth = 2;
ctx.stroke();
const wheelRadius = player.size * 0.35;
const wheelY = carY + carHeight;
ctx.fillStyle = '#0d1221';
ctx.beginPath();
ctx.arc(carX + carWidth * 0.28, wheelY, wheelRadius, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(carX + carWidth * 0.72, wheelY, wheelRadius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fillRect(carX + carWidth * 0.42, carY + carHeight * 0.32, carWidth * 0.22, carHeight * 0.2);
}
spawnEasterEggTarget() {
const canvas = this.elements.easterEggCanvas;
if (!canvas)
return;
const target = this.easterEggGame.target;
const player = this.easterEggGame.player;
const padding = target.size + 12;
let attempts = 0;
do {
target.x = padding + Math.random() * (canvas.width - padding * 2);
target.y = padding + Math.random() * (canvas.height - padding * 2);
attempts++;
} while (Math.hypot(player.x - target.x, player.y - target.y) < player.size * 2 && attempts < 12);
}
updateEasterEggScore() {
if (!this.elements.easterEggScore)
return;
this.elements.easterEggScore.textContent = `Score: ${this.easterEggGame.score}`;
}
sanitizeFilename(value) {
if (typeof value !== 'string')
return '';

View File

@ -563,6 +563,51 @@
}
}
.easter-egg-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
z-index: 999;
}
.easter-egg-card {
background: var(--surface-color);
border-radius: 16px;
padding: 1.25rem;
width: min(640px, 100%);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.easter-egg-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.35rem;
}
.easter-egg-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0.75rem 0;
color: var(--muted-text-color);
}
#easterEggCanvas {
width: 100%;
height: auto;
border-radius: 12px;
background: radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.06), transparent 40%), radial-gradient(circle at 80% 30%, rgba(255, 255, 255, 0.05), transparent 45%), #0d1221;
border: 1px solid rgba(255, 255, 255, 0.08);
display: block;
}
@media (max-width: 820px) {
:root {
--sidebar-width: clamp(160px, 34vw, 220px);
@ -597,7 +642,7 @@
<header>
<div class="header-bar">
<div class="brand">
<img src="styles/pce/icon.svg" alt="EV Dash logo" class="brand-logo">
<img src="styles/pce/icon.svg" alt="EV Dash logo" class="brand-logo" id="brandLogo">
<div class="brand-text">
<h1>EV Dash</h1>
<p>Monitor & troubleshoot EV chargers.</p>
@ -782,6 +827,24 @@
</footer>
</div>
<div id="easterEggOverlay" class="easter-egg-overlay hidden" aria-hidden="true">
<div class="easter-egg-card" role="dialog" aria-modal="true" aria-labelledby="easterEggTitle">
<div class="easter-egg-header">
<div>
<p class="eyebrow">Hidden treat</p>
<h3 id="easterEggTitle">Grid Dash</h3>
</div>
<button type="button" id="easterEggClose" class="ghost">Close</button>
</div>
<p class="helper-text">Use arrow keys or WASD to drive the tiny EV and catch lightning bolts. Press Esc or close to exit.</p>
<div class="easter-egg-meta">
<span id="easterEggScore">Score: 0</span>
<span id="easterEggHint">Stay inside the grid!</span>
</div>
<canvas id="easterEggCanvas" width="520" height="320" aria-label="Mini game canvas"></canvas>
</div>
</div>
<section id="loginOverlay" class="login-view" aria-labelledby="loginTitle">
<div class="login-panel">
<h2 id="loginTitle">Sign in</h2>