Website-fabianschieder/assets/js/main.js

179 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── 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;
const endpoint = './server_status.php';
const normalize = (url) => {
if (!url) return '';
return String(url).replace(/\/+$/, '');
};
const parsePossiblyUtf16Json = (text) => {
if (!text) return null;
const cleaned = String(text)
.replace(/^?\uFEFF/, '')
.replace(/\u0000/g, '')
.trim();
return JSON.parse(cleaned);
};
const applyPayload = (payload) => {
const byUrl = payload && payload.byUrl ? payload.byUrl : {};
const byUrlNormalized = payload && payload.byUrlNormalized ? payload.byUrlNormalized : {};
let applied = 0;
for (const card of cards) {
const key = normalize(card.getAttribute('data-status-url'));
const svc = byUrl[key] || byUrl[key + '/'] || byUrlNormalized[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';
}
applied++;
}
console.debug('[server-status] applied badges:', applied);
};
const fetchAndApply = async () => {
const res = await fetch(endpoint, { cache: 'no-store' });
if (!res.ok) throw new Error('status_fetch_failed_' + res.status);
const text = await res.text();
let payload;
try {
payload = parsePossiblyUtf16Json(text);
} catch (e) {
console.warn('[server-status] JSON parse failed, first 120 chars:', text.slice(0, 120));
throw e;
}
if (!payload) throw new Error('status_json_empty');
applyPayload(payload);
};
const delays = [1000, 1000, 1500];
for (const d of delays) {
await new Promise(r => setTimeout(r, d));
try {
await fetchAndApply();
break;
} catch (e) {
console.warn('[server-status] update failed:', e);
}
}
})();