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:
624
public/wlan.html
Normal file
624
public/wlan.html
Normal 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">💡 Wenn’s 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>
|
||||
Reference in New Issue
Block a user