141 lines
5.9 KiB
JavaScript
141 lines
5.9 KiB
JavaScript
// ── Typewriter ───────────────────────────────────────────
|
|
(function () {
|
|
const el = document.getElementById('typewriter');
|
|
const words = ['Entwickler', 'Schüler', 'Macher', 'Selfhoster', 'Tüftler'];
|
|
let wi = 0, ci = 0, deleting = false;
|
|
|
|
function type() {
|
|
if (!el) return;
|
|
const word = words[wi];
|
|
el.textContent = deleting ? word.slice(0, ci--) : word.slice(0, ci++);
|
|
|
|
let delay = deleting ? 60 : 110;
|
|
if (!deleting && ci > word.length) { delay = 1400; deleting = true; }
|
|
if (deleting && ci < 0) { deleting = false; wi = (wi + 1) % words.length; ci = 0; delay = 300; }
|
|
setTimeout(type, delay);
|
|
}
|
|
setTimeout(type, 900);
|
|
})();
|
|
|
|
// ── Particles ────────────────────────────────────────────
|
|
(function () {
|
|
const canvas = document.getElementById('particle-canvas');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
const COLORS = ['#6366f1', '#0ea5e9', '#a855f7', '#ec4899', '#22d3ee'];
|
|
let W, H, particles = [];
|
|
|
|
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
|
|
function rand(a, b) { return Math.random() * (b - a) + a; }
|
|
|
|
function mkP() {
|
|
return { x: rand(0,W), y: rand(0,H), r: rand(0.8,2.2), dx: rand(-0.25,0.25), dy: rand(-0.35,-0.08),
|
|
alpha: rand(0.2,0.8), fade: rand(0.002,0.006), color: COLORS[Math.floor(Math.random()*COLORS.length)] };
|
|
}
|
|
|
|
function init() { resize(); particles = Array.from({length:90}, mkP); }
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, W, H);
|
|
for (let p of particles) {
|
|
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
|
|
ctx.fillStyle = p.color; ctx.globalAlpha = p.alpha; ctx.fill();
|
|
p.x += p.dx; p.y += p.dy; p.alpha -= p.fade;
|
|
if (p.alpha <= 0 || p.y < -5) Object.assign(p, mkP(), { y: H+5, alpha: rand(0.2,0.7) });
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
requestAnimationFrame(draw);
|
|
}
|
|
|
|
window.addEventListener('resize', resize);
|
|
init(); draw();
|
|
})();
|
|
|
|
// ── 3D Tilt Cards ────────────────────────────────────────
|
|
document.querySelectorAll('.card').forEach(card => {
|
|
card.addEventListener('mousemove', function (e) {
|
|
const rect = this.getBoundingClientRect();
|
|
const cx = rect.left + rect.width / 2;
|
|
const cy = rect.top + rect.height / 2;
|
|
const dx = (e.clientX - cx) / (rect.width / 2);
|
|
const dy = (e.clientY - cy) / (rect.height / 2);
|
|
const rotX = (-dy * 8).toFixed(2);
|
|
const rotY = ( dx * 8).toFixed(2);
|
|
this.style.transform = `perspective(600px) rotateX(${rotX}deg) rotateY(${rotY}deg) translateY(-3px) scale(1.02)`;
|
|
|
|
// glare
|
|
let glare = this.querySelector('.card-glare');
|
|
if (!glare) {
|
|
glare = document.createElement('div');
|
|
glare.className = 'card-glare';
|
|
this.appendChild(glare);
|
|
}
|
|
const gx = ((e.clientX - rect.left) / rect.width * 100).toFixed(1);
|
|
const gy = ((e.clientY - rect.top) / rect.height * 100).toFixed(1);
|
|
glare.style.background = `radial-gradient(circle at ${gx}% ${gy}%, rgba(255,255,255,0.12) 0%, transparent 65%)`;
|
|
});
|
|
|
|
card.addEventListener('mouseleave', function () {
|
|
this.style.transform = '';
|
|
this.querySelector('.card-glare')?.remove();
|
|
});
|
|
|
|
// Ripple
|
|
card.addEventListener('click', function (e) {
|
|
const circle = document.createElement('span');
|
|
const diameter = Math.max(this.clientWidth, this.clientHeight);
|
|
const rect = this.getBoundingClientRect();
|
|
circle.classList.add('ripple');
|
|
circle.style.width = circle.style.height = diameter + 'px';
|
|
circle.style.left = (e.clientX - rect.left - diameter / 2) + 'px';
|
|
circle.style.top = (e.clientY - rect.top - diameter / 2) + 'px';
|
|
this.querySelector('.ripple')?.remove();
|
|
this.appendChild(circle);
|
|
});
|
|
});
|
|
|
|
// ── Server Status Badges (async) ───────────────────────────
|
|
(async function () {
|
|
const cards = Array.from(document.querySelectorAll('.card[data-status-url]'));
|
|
if (cards.length === 0) return;
|
|
|
|
try {
|
|
const res = await fetch('/server_status.php', { cache: 'no-store' });
|
|
if (!res.ok) return;
|
|
const payload = await res.json();
|
|
const byUrl = payload && payload.byUrl ? payload.byUrl : {};
|
|
|
|
// Normalisierung muss zur PHP-Normalisierung passen (Trailing Slash entfernen)
|
|
const normalize = (url) => {
|
|
if (!url) return '';
|
|
return String(url).replace(/\/+$/, '');
|
|
};
|
|
|
|
for (const card of cards) {
|
|
const key = normalize(card.getAttribute('data-status-url'));
|
|
const svc = byUrl[key] || byUrl[key + '/'] || null;
|
|
if (!svc) continue;
|
|
|
|
const state = svc.state || 'unknown';
|
|
const badge = card.querySelector('[data-status-badge="1"]');
|
|
if (!badge) continue;
|
|
|
|
badge.classList.remove('status-badge--unknown', 'status-badge--up', 'status-badge--down');
|
|
badge.classList.add('status-badge--' + state);
|
|
|
|
if (state === 'up') {
|
|
badge.textContent = 'Online';
|
|
badge.title = 'Online';
|
|
} else if (state === 'down') {
|
|
badge.textContent = 'Offline';
|
|
badge.title = svc.detail || 'Offline';
|
|
} else {
|
|
badge.textContent = 'Unbekannt';
|
|
badge.title = svc.detail || 'Unbekannt';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// still fine: keep "Unbekannt"
|
|
}
|
|
})();
|