Website-fabianschieder/index.php

251 lines
9.0 KiB
PHP

<?php
declare(strict_types=1);
/*
* Absolute Webpfade verwenden!
* Ordnerstruktur im Webroot:
* /icons/...
* /style.css
*/
$projects = [
"privat" => [
[
"title" => "Gitea",
"description" => "Mein privater Gitea - Server",
"url" => "/git",
"logo" => "/icons/gitea.svg",
"color" => "#609926",
"external" => true
],
[
"title" => "Nextcloud",
"description" => "Meine persönliche Nextcloud",
"url" => "/nextcloud",
"logo" => "/icons/nextcloud.svg",
"color" => "#0082c9",
"external" => true
],
[
"title" => "Server Dashboard",
"description" => "Server-Verwaltung mit Cockpit",
"url" => "https://cockpit.fabianschieder.com",
"logo" => "/icons/ubuntu.svg",
"color" => "#E95420",
"external" => true
],
],
"schule" => [
[
"title" => "Geizkragen",
"description" => "Ein Online - Preisvergleichsportal",
"url" => "https://geizkragen.store",
"logo" => "/icons/geizkragen.png",
"color" => "#0082c9",
"external" => true
],
],
"dienste" => [
[
"title" => "Home Assistant",
"description" => "Mein privater HomeAssistant Server",
"url" => "http://homeassistant.fabianschieder.com",
"logo" => "/icons/homeassistant.svg",
"color" => "#18BCF2",
"external" => true
],
[
"title" => "NAS",
"description" => "Mein privater Netzwerkspeicher",
"url" => "http://nas.fabianschieder.com",
"logo" => "/icons/nas.svg",
"color" => "#a855f7",
"external" => true
],
],
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fabian Schieder</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<canvas id="particle-canvas"></canvas>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="orb orb-4"></div>
<div class="background-blur"></div>
<header>
<div class="avatar">FS</div>
<h1 class="glitch" data-text="Fabian Schieder">Fabian Schieder</h1>
<p class="tagline" id="typewriter"></p>
</header>
<main>
<?php foreach ($projects as $category => $items): ?>
<section>
<h2 class="category-title">
<?php
switch ($category) {
case "privat":
echo '🔒 Privat';
break;
case "schule":
echo '🎓 Schule';
break;
case "dienste":
echo '🔗 Verlinkungen';
break;
}
?>
</h2>
<div class="cards">
<?php foreach ($items as $project): ?>
<a
href="<?= htmlspecialchars($project['url'], ENT_QUOTES) ?>"
class="card"
<?= !empty($project['external']) ? 'target="_blank" rel="noopener noreferrer"' : '' ?>
style="--accent: <?= htmlspecialchars($project['color'], ENT_QUOTES) ?>;"
>
<div class="card-icon">
<?php if (!empty($project['logo'])): ?>
<img
src="<?= htmlspecialchars($project['logo'], ENT_QUOTES) ?>"
alt="<?= htmlspecialchars($project['title'], ENT_QUOTES) ?> Logo"
class="card-logo"
loading="lazy"
decoding="async"
>
<?php else: ?>
<span aria-hidden="true">📁</span>
<?php endif; ?>
</div>
<div class="card-body">
<h3><?= htmlspecialchars($project['title'], ENT_QUOTES) ?></h3>
<p><?= htmlspecialchars($project['description'], ENT_QUOTES) ?></p>
</div>
<div class="card-arrow" aria-hidden="true">→</div>
</a>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</main>
<footer>
<p>&copy; <?= date('Y') ?> Fabian Schieder — Alle Rechte vorbehalten.</p>
</footer>
<script>
// ── 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() {
const word = words[wi];
const display = deleting ? word.slice(0, ci--) : word.slice(0, ci++);
el.textContent = display;
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');
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);
});
});
</script>
</body>
</html>