Files
fsae41.de/public/wlan.html
BolkeDerBaer 038910e9f0 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.
2026-02-22 00:50:22 +01:00

624 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>