Website-fabianschieder/assets/js/main.js

164 lines
6.4 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);
const accent = getComputedStyle(this).getPropertyValue('--accent').trim() || '#6366f1';
glare.style.background = `radial-gradient(circle at ${gx}% ${gy}%,
color-mix(in srgb, ${accent} 18%, transparent) 0%,
transparent 70%)`;
});
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, no reload) ───────────────
(async function () {
const cards = Array.from(document.querySelectorAll('.card[data-status-url]'));
if (cards.length === 0) return;
const endpoint = '/api/server_status.php';
const normalize = (url) => {
if (!url) return '';
return String(url).replace(/\/+$/, '');
};
const safeJsonParse = (text) => {
const cleaned = String(text || '')
.replace(/^\uFEFF/, '')
.replace(/\u0000/g, '')
.trim();
if (!cleaned) return null;
return JSON.parse(cleaned);
};
const apply = (byUrl) => {
for (const card of cards) {
const key = normalize(card.getAttribute('data-status-url'));
const svc = byUrl[key] || byUrl[key + '/'] || null;
if (!svc) continue;
const badge = card.querySelector('[data-status-badge="1"]');
if (!badge) continue;
const state = svc.state || 'unknown';
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 = 'Lädt...';
badge.title = svc.detail || 'Lädt...';
}
}
};
// Exactly ~1s delay, then fetch.
await new Promise(r => setTimeout(r, 1000));
try {
const res = await fetch(endpoint, { cache: 'no-store' });
if (!res.ok) return;
const text = await res.text();
const payload = safeJsonParse(text);
if (!payload || !payload.byUrlNormalized) return;
apply(payload.byUrlNormalized);
} catch (e) {
// keep "Lädt..."
}
})();