251 lines
9.0 KiB
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 Git - 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://fabianschieder.com:9090",
|
|
"logo" => "/icons/ubuntu.svg",
|
|
"color" => "#E95420",
|
|
"external" => true
|
|
],
|
|
],
|
|
"schule" => [
|
|
[
|
|
"title" => "Geizkragen.store",
|
|
"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>© <?= 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>
|
|
|