Add service worker for push notifications, create calendar layout, and implement WLAN QR code page

- Implemented a service worker (sw.js) to handle push notifications with dynamic options and notification click events.
- Created a calendar layout in test.html with a grid system for displaying events across days and times.
- Developed a visually engaging WLAN QR code page (wlan.html) with animated backgrounds, particle effects, and tips for connecting to the network.
This commit is contained in:
2026-02-22 00:50:22 +01:00
parent 6b96cd2012
commit 038910e9f0
26 changed files with 32980 additions and 5 deletions

624
public/wlan.html Normal file
View File

@@ -0,0 +1,624 @@
<!doctype html>
<html lang="de">
<head>
<meta name="robots" content="noindex">
<meta name="robots" content="nofollow">
<link rel="icon" href="https://fsae41.de/schule.ico" type="image/x-icon">
<script defer src="https://analytics.fsae41.de/script.js" data-website-id="257da02e-d678-47b6-b036-e3bdabaf1405"></script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FSAE41 WLAN QR-Code</title>
<meta name="description" content="Bunt, unnötig animiert, zeigt den WLAN-QR-Code der Klasse FSAE41." />
<style>
:root {
--glowA: 0 0 20px rgba(0, 255, 255, .8);
--glowB: 0 0 28px rgba(255, 0, 255, .8);
--glowC: 0 0 36px rgba(255, 255, 0, .7);
--card: rgba(10, 10, 20, .55);
--card2: rgba(255, 255, 255, .08);
--white: rgba(255, 255, 255, .92);
}
/* ====== Hintergrund: animierter Regenbogen + Noise + Scanlines ====== */
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Noto Sans", "Helvetica Neue", sans-serif;
color: var(--white);
background:
radial-gradient(circle at 20% 20%, rgba(255, 0, 200, .35), transparent 45%),
radial-gradient(circle at 80% 30%, rgba(0, 255, 200, .35), transparent 45%),
radial-gradient(circle at 40% 85%, rgba(255, 255, 0, .25), transparent 50%),
linear-gradient(120deg, #ff0080, #00e5ff, #ffee00, #8a2be2, #00ff8a, #ff3d00);
background-size: 200% 200%;
animation: bgShift 9s ease-in-out infinite alternate;
}
@keyframes bgShift {
0% {
background-position: 0% 30%;
filter: hue-rotate(0deg) saturate(1.2);
}
50% {
background-position: 70% 70%;
filter: hue-rotate(120deg) saturate(1.7);
}
100% {
background-position: 100% 0%;
filter: hue-rotate(260deg) saturate(1.35);
}
}
/* Scanlines */
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(to bottom,
rgba(255, 255, 255, .06) 0px,
rgba(255, 255, 255, .06) 1px,
rgba(0, 0, 0, 0) 3px,
rgba(0, 0, 0, 0) 6px);
mix-blend-mode: overlay;
opacity: .22;
animation: scanFlicker 2.8s infinite;
}
@keyframes scanFlicker {
0%,
100% {
opacity: .15;
transform: translateY(0);
}
50% {
opacity: .28;
transform: translateY(1px);
}
}
/* Noise */
.noise {
position: fixed;
inset: -50%;
pointer-events: none;
background-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
mix-blend-mode: overlay;
opacity: .18;
transform: rotate(8deg);
animation: noiseMove 7s linear infinite;
}
@keyframes noiseMove {
from {
transform: translate3d(-2%, -2%, 0) rotate(8deg);
}
to {
transform: translate3d(2%, 2%, 0) rotate(8deg);
}
}
/* ====== Layout ====== */
.wrap {
position: relative;
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
z-index: 2;
}
.card {
width: min(860px, 92vw);
display: grid;
grid-template-columns: 1.1fr .9fr;
gap: 22px;
padding: 22px;
border-radius: 26px;
background: linear-gradient(180deg, var(--card), rgba(10, 10, 20, .28));
border: 1px solid rgba(255, 255, 255, .18);
box-shadow:
0 25px 80px rgba(0, 0, 0, .45),
var(--glowA),
var(--glowB);
backdrop-filter: blur(12px);
position: relative;
overflow: hidden;
animation: cardFloat 4.2s ease-in-out infinite;
}
@keyframes cardFloat {
0%,
100% {
transform: translateY(0) rotate(-.2deg);
}
50% {
transform: translateY(-10px) rotate(.2deg);
}
}
/* Glitzerband im Card-Hintergrund */
.card::before {
content: "";
position: absolute;
inset: -60%;
background: conic-gradient(from 0deg,
rgba(255, 0, 150, .0),
rgba(255, 255, 0, .25),
rgba(0, 255, 255, .25),
rgba(140, 0, 255, .25),
rgba(0, 255, 140, .25),
rgba(255, 80, 0, .25),
rgba(255, 0, 150, .0));
filter: blur(14px);
opacity: .7;
animation: spin 5.5s linear infinite;
pointer-events: none;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.card::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, .10), transparent 45%),
radial-gradient(circle at 70% 80%, rgba(255, 255, 255, .08), transparent 55%);
pointer-events: none;
}
@media (max-width: 780px) {
.card {
grid-template-columns: 1fr;
}
}
.left {
position: relative;
padding: 10px 10px 10px 6px;
z-index: 1;
}
.badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: linear-gradient(90deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .07));
border: 1px solid rgba(255, 255, 255, .18);
box-shadow: var(--glowC);
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
font-size: 13px;
width: fit-content;
animation: badgeWiggle 2.2s ease-in-out infinite;
}
@keyframes badgeWiggle {
0%,
100% {
transform: rotate(-1deg) scale(1);
}
50% {
transform: rotate(1deg) scale(1.04);
}
}
h1 {
margin: 14px 0 10px;
font-size: clamp(34px, 4.6vw, 56px);
line-height: 1.02;
letter-spacing: -0.02em;
text-shadow: 0 10px 40px rgba(0, 0, 0, .45);
animation: titleHue 3.6s linear infinite;
}
@keyframes titleHue {
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(360deg);
}
}
.sub {
margin: 0 0 14px;
font-size: clamp(14px, 2.0vw, 18px);
opacity: .92;
}
.tips {
display: grid;
gap: 10px;
margin-top: 16px;
}
.tip {
padding: 12px 14px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(255, 255, 255, .11), rgba(255, 255, 255, .06));
border: 1px solid rgba(255, 255, 255, .15);
box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
transform-origin: left center;
animation: tipPop 2.8s ease-in-out infinite;
}
.tip:nth-child(2) {
animation-delay: .35s;
}
.tip:nth-child(3) {
animation-delay: .7s;
}
@keyframes tipPop {
0%,
100% {
transform: translateY(0) rotate(-.15deg) scale(1);
}
50% {
transform: translateY(-6px) rotate(.15deg) scale(1.02);
}
}
.right {
position: relative;
display: grid;
place-items: center;
z-index: 1;
}
.qr-frame {
width: min(340px, 72vw);
aspect-ratio: 1 / 1;
border-radius: 28px;
padding: 18px;
background: linear-gradient(135deg, rgba(255, 255, 255, .18), rgba(255, 255, 255, .06));
border: 1px solid rgba(255, 255, 255, .22);
box-shadow:
0 18px 55px rgba(0, 0, 0, .35),
var(--glowA),
var(--glowB),
var(--glowC);
position: relative;
overflow: hidden;
animation: framePulse 1.8s ease-in-out infinite;
}
@keyframes framePulse {
0%,
100% {
transform: rotate(-.4deg) scale(1);
}
50% {
transform: rotate(.4deg) scale(1.03);
}
}
/* Regenbogenrand-Licht */
.qr-frame::before {
content: "";
position: absolute;
inset: -40%;
background: conic-gradient(from 180deg,
#ff004c, #ffea00, #00ffb7, #00b3ff, #a100ff, #ff004c);
opacity: .55;
filter: blur(18px);
animation: spin 3.2s linear infinite reverse;
pointer-events: none;
}
.qr {
width: 100%;
height: 100%;
border-radius: 18px;
background: #fff;
display: grid;
place-items: center;
position: relative;
overflow: hidden;
}
/* QR selbst */
.qr img {
width: 92%;
height: 92%;
object-fit: contain;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
transform-origin: center;
animation: qrWobble 1.4s ease-in-out infinite;
filter: drop-shadow(0 12px 22px rgba(0, 0, 0, .18));
}
@keyframes qrWobble {
0%,
100% {
transform: rotate(-.7deg) scale(1);
}
50% {
transform: rotate(.7deg) scale(1.02);
}
}
/* Shine-Sweep */
.shine {
position: absolute;
inset: 0;
background: linear-gradient(120deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, .55) 45%,
rgba(255, 255, 255, 0) 70%);
transform: translateX(-140%) rotate(18deg);
mix-blend-mode: screen;
animation: sweep 2.4s ease-in-out infinite;
pointer-events: none;
}
@keyframes sweep {
0% {
transform: translateX(-140%) rotate(18deg);
opacity: 0;
}
18% {
opacity: .75;
}
55% {
opacity: .55;
}
100% {
transform: translateX(140%) rotate(18deg);
opacity: 0;
}
}
/* ====== Partikel-Canvas ====== */
canvas#party {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
mix-blend-mode: screen;
opacity: .85;
}
/* ====== Footer Bling ====== */
.footer {
margin-top: 16px;
font-size: 12px;
opacity: .85;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.pill {
padding: 8px 10px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, .18);
background: rgba(255, 255, 255, .08);
animation: pillBounce 1.7s ease-in-out infinite;
}
.pill:nth-child(2) {
animation-delay: .2s;
}
.pill:nth-child(3) {
animation-delay: .4s;
}
@keyframes pillBounce {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
/* Accessibility-ish: weniger Bewegung */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
body {
background-size: 100% 100%;
}
}
</style>
</head>
<body>
<canvas id="party"></canvas>
<div class="noise"></div>
<div class="scanlines"></div>
<main class="wrap">
<section class="card" aria-label="FSAE41 WLAN QR Code">
<div class="left">
<div class="badge">📶 FSAE41 • Schnelles-WLAN! • QR-Code</div>
<h1>Scan mich<br />für WLAN ✨</h1>
<p class="sub">
JETZT QR<br>Code scannen und mit bis zu 1Gbit/s <em>los surfen!</em>.
</p>
<div class="tips">
<div class="tip">✅ Kamera-App öffnen → QR scannen → verbinden</div>
<div class="tip">💡 Wenns nicht klappt: Abstand ändern / Licht an</div>
<div class="tip">🚀 Bonus: <b>SSID: FSAE41.de | Pass: FSAE41@bbs (WPA2)</b></div>
</div>
<div class="footer">
<span class="pill"><b><a href="https://www.fsae41.de">HOME</a></b></span>
<span class="pill"><b><a href="https://www.lifab.de/OT">die OT</a></b></span>
<span class="pill" id="ip">⏱️</span>
</div>
</div>
<div class="right">
<div class="qr-frame" title="QR-Code: qr-code_fsae41.png">
<div class="qr">
<img src="static/qr-code_fsae41.png" alt="QR Code für das Klassen-WLAN FSAE41" />
<div class="shine"></div>
</div>
</div>
</div>
</section>
</main>
<script>
// IP anzeigen
fetch("https://api.ipify.org?format=json")
.then(r => r.json())
.then(data => {
document.getElementById("ip").textContent = data.ip;
})
.catch(() => {
document.getElementById("ip").textContent = "Fehler beim Laden";
});
// ====== Partikel-Party im Canvas ======
const canvas = document.getElementById("party");
const ctx = canvas.getContext("2d", { alpha: true });
function resize() {
const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
canvas.width = Math.floor(window.innerWidth * dpr);
canvas.height = Math.floor(window.innerHeight * dpr);
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();
const rand = (a, b) => a + Math.random() * (b - a);
// Bunte „Konfetti“-Partikel + Orbit-Bubbles
const particles = [];
const N = Math.min(180, Math.floor((window.innerWidth * window.innerHeight) / 12000));
function makeParticle() {
const type = Math.random() < 0.72 ? "confetti" : "bubble";
return {
type,
x: rand(0, window.innerWidth),
y: rand(0, window.innerHeight),
vx: rand(-0.6, 0.6),
vy: rand(-1.2, -0.2),
size: type === "confetti" ? rand(2, 6) : rand(6, 16),
rot: rand(0, Math.PI * 2),
vr: rand(-0.08, 0.08),
hue: rand(0, 360),
life: rand(220, 520),
t: 0,
wobble: rand(0.6, 2.2),
phase: rand(0, Math.PI * 2),
};
}
for (let i = 0; i < N; i++) particles.push(makeParticle());
let mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2;
window.addEventListener("pointermove", (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
}, { passive: true });
function draw() {
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
// Leichte „Aura“ um den Mauszeiger (komplett unnötig)
const grad = ctx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, 180);
grad.addColorStop(0, "rgba(255,255,255,0.22)");
grad.addColorStop(1, "rgba(255,255,255,0)");
ctx.fillStyle = grad;
ctx.fillRect(mouseX - 180, mouseY - 180, 360, 360);
for (const p of particles) {
p.t += 1;
p.life -= 1;
p.rot += p.vr;
// Bewegung
const wob = Math.sin((p.t * 0.03) + p.phase) * p.wobble;
p.x += p.vx + wob * 0.05;
p.y += p.vy + Math.cos((p.t * 0.02) + p.phase) * 0.08;
// Wieder oben rein
if (p.y < -40 || p.x < -60 || p.x > window.innerWidth + 60 || p.life <= 0) {
Object.assign(p, makeParticle(), { y: window.innerHeight + rand(0, 120) });
}
// Zeichnen
const a = 0.55 + 0.35 * Math.sin(p.t * 0.02 + p.phase);
if (p.type === "confetti") {
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = `hsla(${p.hue}, 95%, 60%, ${a})`;
ctx.fillRect(-p.size, -p.size / 2, p.size * 2.2, p.size);
ctx.restore();
} else {
ctx.beginPath();
ctx.fillStyle = `hsla(${p.hue}, 95%, 65%, ${a * 0.55})`;
ctx.arc(p.x, p.y, p.size * (0.55 + 0.25 * Math.sin(p.t * 0.03)), 0, Math.PI * 2);
ctx.fill();
}
}
requestAnimationFrame(draw);
}
draw();
// Kleines Easter Egg: Space = "Turbo Disco"
let turbo = false;
window.addEventListener("keydown", (e) => {
if (e.code === "Space") {
turbo = !turbo;
document.body.style.animationDuration = turbo ? "2.8s" : "9s";
}
});
</script>
</body>
</html>