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:
418
public/ausbildung_quiz.html
Normal file
418
public/ausbildung_quiz.html
Normal file
@@ -0,0 +1,418 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Quiz: Ausbildung</title>
|
||||
<script src="/lib/pocketbase.umd.js"></script>
|
||||
<script defer src="https://analytics.fsae41.de/script.js" data-website-id="257da02e-d678-47b6-b036-e3bdabaf1405"></script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f7fafc;
|
||||
--card: #ffffff;
|
||||
--accent: #2563eb;
|
||||
--muted: #6b7280;
|
||||
--correct: #16a34a;
|
||||
--wrong: #ef4444
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
|
||||
background: var(--bg);
|
||||
padding: 18px
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
|
||||
padding: 18px;
|
||||
max-width: 760px
|
||||
}
|
||||
|
||||
.q-head {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start
|
||||
}
|
||||
|
||||
.q-id {
|
||||
display: none
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px
|
||||
}
|
||||
|
||||
p.meta {
|
||||
margin: 0 0 14px 0;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
p.intro {
|
||||
margin-bottom: 12px;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
ul.answers {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
ul.answers li {
|
||||
border: 1px solid #e6e9ef;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all .12s
|
||||
}
|
||||
|
||||
ul.answers li.selected {
|
||||
border-color: var(--accent);
|
||||
background: rgba(37, 99, 235, 0.06)
|
||||
}
|
||||
|
||||
ul.answers li:hover {
|
||||
transform: translateY(-2px)
|
||||
}
|
||||
|
||||
ul.answers li.correct {
|
||||
border-color: var(--correct);
|
||||
background: rgba(16, 185, 129, 0.06)
|
||||
}
|
||||
|
||||
ul.answers li.incorrect {
|
||||
border-color: var(--wrong);
|
||||
background: rgba(239, 68, 68, 0.06)
|
||||
}
|
||||
|
||||
.badge-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4px
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.control-right {
|
||||
display: flex;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 0;
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--accent);
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid #e6e9ef
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: var(--wrong);
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--muted)
|
||||
}
|
||||
|
||||
#report-box {
|
||||
display: none;
|
||||
margin-top: 12px
|
||||
}
|
||||
|
||||
#report-box textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
border: 1px solid #e6e9ef;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
font-family: inherit
|
||||
}
|
||||
|
||||
#report-box button {
|
||||
margin-top: 8px
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
margin-bottom: 12px
|
||||
}
|
||||
|
||||
select#question-select {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e6e9ef;
|
||||
font-family: inherit
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="quiz-card" class="card" role="region" aria-label="Ausbildungsfrage">
|
||||
<div class="dropdown-container">
|
||||
<label for="question-select">Frage auswählen ID: </label>
|
||||
<select id="question-select"></select>
|
||||
</div>
|
||||
|
||||
<p class="intro" id="q-textIntro">Als Ausbilder setzen Sie zur Anleitung der Auszubildenden auch die
|
||||
Vier-Stufen-Methode ein.</p>
|
||||
<div class="q-head">
|
||||
<div class="q-id" id="q-id">ID 344</div>
|
||||
<div style="flex:1">
|
||||
<h2 id="q-text">Welches der folgenden Merkmale trifft auf die erste Stufe zu?</h2>
|
||||
<p class="meta" id="q-category">Kategorie: 3</p>
|
||||
<div class="badge-container">
|
||||
<p class="badge" id="q-type">1 Antwort richtig</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="intro" id="answersIntro">Die Auszubildenden sollen…</p>
|
||||
<ul id="answers" class="answers" role="list">
|
||||
<!-- Antworten werden hier gerendert -->
|
||||
</ul>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn danger" id="report-error">Fehler melden</button>
|
||||
<button class="btn" id="last-q">Zurück</button>
|
||||
<div class="control-right">
|
||||
<button class="btn" id="next-q">Weiter</button>
|
||||
<button class="btn ghost" id="clear-selection">Auswahl zurücksetzen</button>
|
||||
<button class="btn primary" id="show-correct">Korrekte Antwort anzeigen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="report-box" style="display: none;">
|
||||
<textarea id="report-text" placeholder="Bitte beschreiben Sie den Fehler..."></textarea>
|
||||
<button class="btn primary" id="send-report">Absenden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const state = { item: null, selected: [], allQuestions: [] };
|
||||
|
||||
function populateDropdown() {
|
||||
const select = document.getElementById('question-select');
|
||||
select.innerHTML = '';
|
||||
state.allQuestions.forEach(q => {
|
||||
const option = document.createElement('option');
|
||||
option.value = q.id;
|
||||
option.textContent = `${q.id}`;
|
||||
option.selected = (state.item && state.item.id === q.id);
|
||||
select.appendChild(option);
|
||||
});
|
||||
select.addEventListener('change', () => {
|
||||
const selectedId = select.value;
|
||||
const question = state.allQuestions.find(q => q.id == selectedId);
|
||||
if (question) window.updateQuiz(question);
|
||||
});
|
||||
}
|
||||
|
||||
function shuffle(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
state.item = data;
|
||||
state.selected = [];
|
||||
document.getElementById('q-id').textContent = 'ID ' + (data.id ?? '–');
|
||||
document.getElementById('q-text').textContent = data.text ?? '';
|
||||
document.getElementById('q-textIntro').innerHTML = data.textIntro || ' ';
|
||||
if (data.textIntro) {
|
||||
document.getElementById('q-textIntro').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('q-textIntro').style.display = 'none';
|
||||
}
|
||||
document.getElementById('q-category').textContent = 'Kategorie: ' + (data.category ?? '–');
|
||||
document.getElementById('answersIntro').innerHTML = data.answersIntro || ' ';
|
||||
if (data.answersIntro) {
|
||||
document.getElementById('answersIntro').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('answersIntro').style.display = 'none';
|
||||
}
|
||||
|
||||
const correctCount = (data.answers || []).filter(a => a.correct).length;
|
||||
document.getElementById('q-type').textContent = `${correctCount} Antwort${correctCount !== 1 ? 'en' : ''} richtig`;
|
||||
const answersEl = document.getElementById('answers');
|
||||
answersEl.innerHTML = '';
|
||||
let answers = [...(data.answers || [])];
|
||||
//answers = shuffle(answers);
|
||||
answers.forEach((a, i) => {
|
||||
const li = document.createElement('li');
|
||||
li.setAttribute('role', 'button');
|
||||
li.tabIndex = 0;
|
||||
li.dataset.index = i;
|
||||
li.dataset.answerId = a.id;
|
||||
li.innerHTML = `<div>${a.text}</div>`;
|
||||
li.addEventListener('click', () => toggleAnswer(i));
|
||||
li.addEventListener('keydown', (e) => { if (e.key === "Enter" || e.key === " ") toggleAnswer(i) });
|
||||
answersEl.appendChild(li);
|
||||
});
|
||||
const lis = document.querySelectorAll('#answers li');
|
||||
lis.forEach(li => { li.classList.remove('incorrect', 'correct', 'selected'); li.style.boxShadow = 'none' });
|
||||
state.item.shuffledAnswers = answers;
|
||||
}
|
||||
|
||||
function toggleAnswer(index) {
|
||||
const li = document.querySelectorAll('#answers li')[index];
|
||||
if (state.selected.includes(index)) {
|
||||
state.selected = state.selected.filter(i => i !== index);
|
||||
li.classList.remove('selected');
|
||||
} else {
|
||||
state.selected.push(index);
|
||||
li.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
window.updateQuiz = function (data) {
|
||||
if (data && data.answers) {
|
||||
data.answers = data.answers.map(a => ({ id: a.id, text: a.text, correct: !!a.correct }));
|
||||
}
|
||||
render(data);
|
||||
};
|
||||
|
||||
window.showCorrect = function () {
|
||||
if (!state.item) return;
|
||||
const lis = document.querySelectorAll('#answers li');
|
||||
lis.forEach((li, i) => {
|
||||
const ans = state.item.answers[i];
|
||||
if (ans && ans.correct) {
|
||||
li.classList.add('correct');
|
||||
if (state.selected.includes(i)) li.classList.add('selected');
|
||||
} else if (state.selected.includes(i)) {
|
||||
li.classList.add('incorrect');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.setCorrect = function ({ index = null, answerId = null } = {}) {
|
||||
if (!state.item) return;
|
||||
|
||||
const answers = state.item.answers;
|
||||
if (index == null && answerId == null) return;
|
||||
answers.forEach((a, i) => a.correct = ((index != null && i === index) || (answerId != null && a.id === answerId)));
|
||||
|
||||
render(state.item);
|
||||
};
|
||||
|
||||
document.getElementById('show-correct').addEventListener('click', () => window.showCorrect());
|
||||
document.getElementById('clear-selection').addEventListener('click', () => {
|
||||
state.selected = [];
|
||||
const lis = document.querySelectorAll('#answers li');
|
||||
lis.forEach(li => { li.classList.remove('incorrect', 'correct', 'selected'); li.style.boxShadow = 'none' });
|
||||
});
|
||||
|
||||
const nextbt = document.getElementById("next-q");
|
||||
const lastbt = document.getElementById("last-q");
|
||||
|
||||
nextbt.addEventListener("click", () => {
|
||||
const select = document.getElementById('question-select');
|
||||
|
||||
// Nur weitergehen, wenn es noch ein nächstes Element gibt
|
||||
if (select.selectedIndex < select.options.length - 1) {
|
||||
select.selectedIndex++;
|
||||
const selectedId = select.value;
|
||||
const question = state.allQuestions.find(q => q.id == selectedId);
|
||||
if (question) window.updateQuiz(question);
|
||||
}
|
||||
})
|
||||
|
||||
lastbt.addEventListener("click", () => {
|
||||
const select = document.getElementById('question-select');
|
||||
|
||||
if (select.selectedIndex > 0) {
|
||||
select.selectedIndex--;
|
||||
const selectedId = select.value;
|
||||
const question = state.allQuestions.find(q => q.id == selectedId);
|
||||
if (question) window.updateQuiz(question);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
const reportBtn = document.getElementById('report-error');
|
||||
const reportBox = document.getElementById('report-box');
|
||||
const sendReport = document.getElementById('send-report');
|
||||
reportBtn.addEventListener('click', () => {
|
||||
reportBox.style.display = reportBox.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
sendReport.addEventListener('click', async () => {
|
||||
const text = document.getElementById('report-text').value.trim();
|
||||
if (text) {
|
||||
console.log('Fehlerbericht gesendet:', text);
|
||||
alert('Vielen Dank für Ihre Rückmeldung!');
|
||||
const data = {
|
||||
"question": state.item.id,
|
||||
"text": text
|
||||
};
|
||||
|
||||
const record = await pb.collection('ADA_report').create(data);
|
||||
|
||||
document.getElementById('report-text').value = '';
|
||||
reportBox.style.display = 'none';
|
||||
} else {
|
||||
alert('Bitte geben Sie eine Fehlerbeschreibung ein.');
|
||||
}
|
||||
});
|
||||
|
||||
// Beispiel-Initialisierung mit mehreren Fragen
|
||||
state.allQuestions = [];
|
||||
|
||||
let pb = new PocketBase();
|
||||
|
||||
(async () => {
|
||||
const records = await pb.collection('ADA_question').getFullList();
|
||||
console.log(records);
|
||||
|
||||
records.forEach(r => {
|
||||
const item = r;
|
||||
state.allQuestions.push(item);
|
||||
});
|
||||
|
||||
let r = Math.floor(Math.random() * state.allQuestions.length);
|
||||
|
||||
window.updateQuiz(state.allQuestions[r]);
|
||||
|
||||
populateDropdown();
|
||||
})();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
public/freigabe/Stundentafel-Abendform-2022.pdf
Normal file
BIN
public/freigabe/Stundentafel-Abendform-2022.pdf
Normal file
Binary file not shown.
@@ -1,11 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSite</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>FSAE41.de</title>
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="robots" content="nofollow">
|
||||
|
||||
<link rel="icon" href="/schule.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
|
||||
<script defer src="https://analytics.fsae41.de/script.js"
|
||||
data-website-id="257da02e-d678-47b6-b036-e3bdabaf1405"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-teal.css">
|
||||
<link rel="stylesheet" href="lib/css/mobiscroll.javascript.min.css">
|
||||
<link rel="stylesheet/less" type="text/css" href="index.less" />
|
||||
|
||||
|
||||
<script src="lib/pocketbase.umd.js"></script>
|
||||
<script src="lib/mobiscroll.javascript.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/less"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
|
||||
<script defer src="index.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>MOIN</h1>
|
||||
|
||||
|
||||
<!-- Header -->
|
||||
<header class="w3-container w3-theme w3-padding">
|
||||
<div class="w3-center">
|
||||
<h1 class="w3-xxxlarge">FSAE41.de</h1>
|
||||
<p>Informationen, Arbeiten & Hilfe</p>
|
||||
<p>R * U * I = 0 =R = U * I</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="w3-row-padding w3-margin-top">
|
||||
|
||||
<!-- Box 1: Linke Seite -->
|
||||
<div class="w3-quarter">
|
||||
|
||||
<div class="w3-card w3-white w3-padding">
|
||||
<h3 class="w3-text-teal">BBS</h3>
|
||||
<ul class="w3-ul">
|
||||
<li><a href="https://bbs-brinkstrasse.moodle-nds.de/">Moodle</a></li>
|
||||
<li><a href="https://bbs-brinkstrasse.webuntis.com/WebUntis/?school=bbs-brinkstrasse">WebUntis</a></li>
|
||||
<li><a href="/freigabe/Stundentafel-Abendform-2022.pdf">Stundentafel</a></li>
|
||||
<li><a href="https://virtueller-stundenplan.org/page2/page-22/" target="_blank">Digit. Schülerausweis</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w3-card w3-white w3-padding w3-margin-top">
|
||||
<h3 class="w3-text-teal">AEVO</h3>
|
||||
<ul class="w3-ul">
|
||||
<!--<li><a href="/ausbildung_quiz.html">AEVO Fragen</a></li>-->
|
||||
<li><a href="https://www.bibb.de/dienst/berufesuche/de/index_berufesuche.php" target="_blank">BIBB AEVO</a>
|
||||
</li>
|
||||
<li><a href="https://www.ihk.de/osnabrueck/" target="_blank">IHK-OSNA</a> / <a
|
||||
href="https://www.hwk-osnabrueck.de/" target="_blank">HWK-OSNA</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w3-card w3-white w3-padding w3-margin-top">
|
||||
<h3 class="w3-text-teal">Services</h3>
|
||||
<ul class="w3-ul">
|
||||
<li><a href="/wlan.html" target="_blank">FSAE41 WLAN</a></li>
|
||||
<li><a href="https://link.fsae41.de/" target="_blank">LinkShare</a></li>
|
||||
<li><a href="https://etherpad.fsae41.de/" target="_blank">Etherpad</a></li>
|
||||
<li><a href="https://analytics.fsae41.de/share/dGTijC1mnk4dU4iZ" target="_blank">Web Analytics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="w3-card w3-white w3-padding w3-margin-top">
|
||||
<h3 class="w3-text-teal">Nützliche Links</h3>
|
||||
<ul class="w3-ul">
|
||||
<li><a href="https://www.falstad.com/circuit/" target="_blank">falstad - Web "MultiSim"</a></li>
|
||||
<li><a href="/old/index.html" target="_blank">Alte Website</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box 2: -->
|
||||
<div class="w3-half">
|
||||
<div class="w3-card w3-white w3-padding w3-margin-bottom">
|
||||
<h1 id="main-heading" class="w3-text-teal w3-round-large w3-center w3-lime w3-padding-12"></h1>
|
||||
<h3 id="target-info" class="w3-center"></h3>
|
||||
<h2 id="countdown" class="w3-center"></h2>
|
||||
<div id="totals" class="w3-center"></div>
|
||||
|
||||
<!--<iframe src="/countdown.html" frameborder="0" style="width:100%;"></iframe>-->
|
||||
</div>
|
||||
<div class="mainbox w3-margin-bottom">
|
||||
<img src="https://fsae41.de/random/pic" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box 3: -->
|
||||
<div class="w3-quarter">
|
||||
<!--
|
||||
<div class="mainbox w3-margin-bottom">
|
||||
<img src="https://fsae41.de/random/pic" style="width:100%;max-width:600px">
|
||||
</div>
|
||||
-->
|
||||
<div class="mainbox events" id="events">
|
||||
<h3>Termine</h3>
|
||||
</div>
|
||||
<!--
|
||||
<div class="w3-card w3-white w3-padding w3-margin-top" id="events">
|
||||
<h3 class="w3-text-teal">Stundenplan</h3>
|
||||
</div>
|
||||
-->
|
||||
<div class="mainbox w3-margin-top">
|
||||
<h3 class="w3-text-teal">Sonder Funktionen</h3>
|
||||
<button class="w3-button w3-teal w3-margin-bottom w3-round" onclick="openAuth()">Login</button>
|
||||
<button class="w3-button w3-teal w3-margin-bottom w3-round" onclick="addPic()">Foto hochladen</button>
|
||||
<button id="notify-btn" class="w3-button w3-teal w3-margin-bottom w3-round">Notify me!</button>
|
||||
<script>
|
||||
const VAPID_PUBLIC_KEY = 'BPPom1qN0L5NF90gxpABHTY2gjXAgCYVvPwXYdfrwxJ1O26Za9A80f7ZcCy6P8vwzbT8dhMvrFzWUjuYN136IFA';
|
||||
|
||||
// Button Event
|
||||
document.getElementById('notify-btn').addEventListener('click', async () => {
|
||||
if (!("Notification" in window)) {
|
||||
alert("Dieser Browser unterstützt keine Desktop-Benachrichtigungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Notification permission anfragen
|
||||
const permission = await Notification.requestPermission();
|
||||
if (permission !== "granted") {
|
||||
alert("Benachrichtigungen wurden nicht erlaubt.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Service Worker registrieren
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
alert("Service Worker werden in diesem Browser nicht unterstützt.");
|
||||
return;
|
||||
}
|
||||
|
||||
const name = await prompt("Wie ist dein Name?") || "Gast";
|
||||
|
||||
if (name == "Gast") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('lib/sw.js')
|
||||
console.log('Service Worker registriert:', registration);
|
||||
|
||||
// Push Subscription erstellen
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
|
||||
});
|
||||
console.log('Push Subscription:', subscription);
|
||||
|
||||
|
||||
|
||||
let payload = JSON.parse(JSON.stringify(subscription));
|
||||
payload.username = name;
|
||||
|
||||
// Subscription zum Server senden
|
||||
await fetch('/save-subscription', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
//Sofortige Notification als Test
|
||||
new Notification(`Danke ${name}!`, {
|
||||
body: "Du hast die Benachrichtigungen erfolgreich aktiviert."
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fehler bei Service Worker oder Push:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: VAPID Key in Uint8Array konvertieren
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
|
||||
const rawData = atob(base64);
|
||||
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="w3-container w3-theme w3-center w3-padding-16 w3-margin-top">
|
||||
<p>FSAE41.de Startseite<br>Danke an
|
||||
<a href="https://lifab.de/OT" class="rainbow-text" style="font-size: 42px; text-decoration: underline;">die
|
||||
OT</a>
|
||||
und ChatGPT
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<div class="modul">
|
||||
<!--Login/Sigup Page-->
|
||||
<div id="auth-module">
|
||||
<div class="auth-box" id="auth-box">
|
||||
<button class="close-btn" onclick="closeAuth()">X</button>
|
||||
<form id="loginForm">
|
||||
<h2>Login</h2>
|
||||
<input type="email" placeholder="E-Mail" required />
|
||||
<input type="password" placeholder="Passwort" required />
|
||||
<button type="submit">Einloggen</button>
|
||||
<div class="switch">Noch kein Konto? <span onclick="toggleForms()">Registrieren</span></div>
|
||||
</form>
|
||||
|
||||
<form id="signupForm" class="hidden">
|
||||
<h2>Signup</h2>
|
||||
<input type="text" placeholder="Name" required />
|
||||
<input type="email" placeholder="E-Mail" required />
|
||||
<input type="password" id="signupPassword" placeholder="Passwort" required />
|
||||
<input type="password" id="signupPasswordConfirm" placeholder="Passwort wiederholen" required />
|
||||
<button type="submit">Registrieren</button>
|
||||
<div class="switch">Schon ein Konto? <span onclick="toggleForms()">Login</span></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="sc">
|
||||
fetch('/random/background').then(response => response.text()).then(d => {
|
||||
if (d == "true") {
|
||||
let x = document.createElement("img");
|
||||
x.src = '/static/nico.png?' + new Date().getTime();
|
||||
x.style.position = "fixed";
|
||||
x.style.top = "0";
|
||||
x.style.left = "0";
|
||||
x.style.width = "100%";
|
||||
x.style.height = "100%";
|
||||
document.body.appendChild(x);
|
||||
|
||||
x.onload = () => {
|
||||
setTimeout(() => {
|
||||
x.style.transition = "opacity 5s";
|
||||
x.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(x);
|
||||
}, 5100);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// remove this script tag after execution
|
||||
document.getElementById("sc").outerHTML = ""
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
339
public/index.js
Normal file
339
public/index.js
Normal file
@@ -0,0 +1,339 @@
|
||||
const PB = new PocketBase();
|
||||
|
||||
// Modul-funktion
|
||||
function toggleForms() {
|
||||
document.getElementById('loginForm').classList.toggle('hidden');
|
||||
document.getElementById('signupForm').classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function openAuth() {
|
||||
document.getElementById('auth-module').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAuth() {
|
||||
document.getElementById('auth-module').style.display = 'none';
|
||||
}
|
||||
|
||||
function shakeBox() {
|
||||
const box = document.getElementById('auth-box');
|
||||
box.classList.add('shake');
|
||||
setTimeout(() => box.classList.remove('shake'), 300);
|
||||
}
|
||||
|
||||
// Klick außerhalb des auth-box schließt das Modul
|
||||
document.getElementById('auth-module').addEventListener('click', function (e) {
|
||||
if (e.target === this) {
|
||||
closeAuth();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let email = e.target[0].value
|
||||
let passwort = e.target[1].value
|
||||
let authData = null;
|
||||
try {
|
||||
authData = await PB.collection('users').authWithPassword(email, passwort);
|
||||
} catch (error) {
|
||||
shakeBox();
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(PB.authStore.isValid);
|
||||
console.log(PB.authStore.token);
|
||||
console.log(PB.authStore.record.id);
|
||||
|
||||
closeAuth();
|
||||
});
|
||||
|
||||
document.getElementById('signupForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let r = null;
|
||||
let authData = null;
|
||||
const data = {
|
||||
"name": e.target[0].value,
|
||||
"email": e.target[1].value,
|
||||
"password": e.target[2].value,
|
||||
"passwordConfirm": e.target[3].value
|
||||
};
|
||||
try {
|
||||
r = await PB.collection('users').create(data);
|
||||
} catch (error) {
|
||||
shakeBox();
|
||||
console.log(error);
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
authData = await PB.collection('users').authWithPassword(data.email, data.passwort);
|
||||
} catch (error) {
|
||||
shakeBox();
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
closeAuth();
|
||||
});
|
||||
|
||||
|
||||
// Clock funktion
|
||||
// TODO: getHtml from Server ??
|
||||
function formatDate(date) {
|
||||
return `${padZero(date.getDate(), 2)}.${padZero(date.getMonth() + 1, 2)}.${date.getFullYear()} um ${padZero(date.getHours(), 2)}:${padZero(date.getMinutes(), 2)}:${padZero(date.getSeconds(), 2)}`;
|
||||
}
|
||||
|
||||
function padZero(num, places) {
|
||||
return num.toString().padStart(places, '0');
|
||||
}
|
||||
|
||||
function pad(d) {
|
||||
return (d < 10) ? '0' + d.toString() : d.toString();
|
||||
}
|
||||
|
||||
async function get_moodle() {
|
||||
let r = await fetch("/moodle/getClasses")
|
||||
let d = await r.json()
|
||||
return d
|
||||
}
|
||||
|
||||
function getNextOrCurrentLesson(data, now = new Date()) {
|
||||
let nextLesson = null;
|
||||
|
||||
for (const dateKey in data) {
|
||||
for (const entry of data[dateKey]) {
|
||||
const year = Number(dateKey.slice(0, 4));
|
||||
const month = Number(dateKey.slice(4, 6)) - 1;
|
||||
const day = Number(dateKey.slice(6, 8));
|
||||
|
||||
const startHour = Math.floor(entry.startTime / 100);
|
||||
const startMin = entry.startTime % 100;
|
||||
const endHour = Math.floor(entry.endTime / 100);
|
||||
const endMin = entry.endTime % 100;
|
||||
|
||||
const start = new Date(year, month, day, startHour, startMin);
|
||||
const end = new Date(year, month, day, endHour, endMin);
|
||||
|
||||
// 🟢 Stunde läuft gerade
|
||||
if (now >= start && now < end) {
|
||||
return { start, end, entry, status: "running" };
|
||||
}
|
||||
|
||||
// 🔵 Stunde kommt noch
|
||||
if (start > now) {
|
||||
if (!nextLesson || start < nextLesson.start) {
|
||||
nextLesson = { start, end, entry, status: "next" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextLesson;
|
||||
}
|
||||
|
||||
function render_countdow_v2(two = false) {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const time = pad(now.getHours().toString()) + ':' + pad(now.getMinutes().toString());
|
||||
let target;
|
||||
|
||||
if (!nextClass) {
|
||||
requestAnimationFrame(render_countdow_v2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextClass.status === "running") {
|
||||
target = nextClass.end;
|
||||
} else if (nextClass.status === "next") {
|
||||
target = nextClass.start;
|
||||
}
|
||||
|
||||
|
||||
const distance = Math.abs(target - now);
|
||||
document.getElementById("target-info").innerHTML = `Bis zum ${formatDate(target)} Uhr sind es noch:`;
|
||||
|
||||
// Display Label
|
||||
if (nextClass.status === "running") {
|
||||
document.getElementById("main-heading").textContent = `"${nextClass.entry.su[0].longname}" in Raum ${nextClass.entry.ro[0].name} findet grade statt`;
|
||||
} else if (nextClass.status === "next") {
|
||||
document.getElementById("main-heading").textContent = `Nächstes Stunde: ${nextClass.entry.su[0].longname} in Raum ${nextClass.entry.ro[0].name}`;
|
||||
}
|
||||
|
||||
const days = distance / (1000 * 60 * 60 * 24);
|
||||
const hours = (distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60);
|
||||
const minutes = (distance % (1000 * 60 * 60)) / (1000 * 60);
|
||||
const seconds = (distance % (1000 * 60)) / 1000;
|
||||
const milliseconds = distance % 1000;
|
||||
|
||||
// Calculate total values
|
||||
const totalDays = days;
|
||||
const totalWeeks = Math.floor(totalDays / 7);
|
||||
const totalHours = totalDays * 24;
|
||||
const totalMinust = totalHours * 60;
|
||||
const totalSeconds = totalMinust * 60;
|
||||
const totalYears = (totalDays / 365).toFixed(2); // Calculate total years to 2 decimal places
|
||||
|
||||
// Display countdown/countup
|
||||
document.getElementById("countdown").innerHTML = `${Math.floor(days)}d ${Math.floor(hours)}h ${Math.floor(minutes)}m ${Math.floor(seconds)}s ${padZero(milliseconds, 3)}ms`;
|
||||
|
||||
// Display totals with thousand separators
|
||||
document.getElementById("totals").innerHTML = `Tage: ${totalDays.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} | Stunden: ${totalHours.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} | Minuten: ${totalMinust.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} | Sekunden: ${totalSeconds.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`;
|
||||
|
||||
requestAnimationFrame(render_countdow_v2);
|
||||
}
|
||||
|
||||
async function init_countdown() {
|
||||
document.getElementById("main-heading").textContent = "Lade Stundenplan...";
|
||||
nextClass = getNextOrCurrentLesson(await get_moodle());
|
||||
|
||||
// Update next class every second
|
||||
setInterval(async () => { nextClass = getNextOrCurrentLesson(await get_moodle()); }, 1000);
|
||||
|
||||
// Start the render loop
|
||||
render_countdow_v2();
|
||||
}
|
||||
|
||||
let nextClass = null;
|
||||
|
||||
init_countdown();
|
||||
|
||||
// fotos hochladen
|
||||
async function addPic() {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.multiple = false;
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.click();
|
||||
// listen to file input changes and add the selected files to the form data
|
||||
fileInput.addEventListener('change', async function () {
|
||||
const formData = new FormData();
|
||||
// set regular text field
|
||||
formData.append('alt', "demo" || prompt("Bitte eine Bildbeschreibung eingeben:"));
|
||||
formData.append('gewicht', 1);
|
||||
formData.append('allowed', false);
|
||||
|
||||
for (let file of fileInput.files) {
|
||||
formData.append('img', file);
|
||||
}
|
||||
const createdRecord = await PB.collection('images').create(formData);
|
||||
alert("Bild erfolgreich hochgeladen!");
|
||||
});
|
||||
}
|
||||
|
||||
// Render event Data
|
||||
function render_event(data) {
|
||||
const eventContainer = document.getElementById('events');
|
||||
|
||||
const eventDiv = document.createElement('div');
|
||||
eventDiv.classList.add('event');
|
||||
|
||||
const dateDiv = document.createElement('div');
|
||||
dateDiv.classList.add('date');
|
||||
const eventDate = new Date(data.start);
|
||||
const monthNames = ["JAN", "FEB", "MÄR", "APR", "MAI", "JUN", "JUL", "AUG", "SEP", "OKT", "NOV", "DEZ"];
|
||||
dateDiv.innerHTML = `${monthNames[eventDate.getMonth()]}<br><span>${eventDate.getDate()}</span>`;
|
||||
if (eventDate.getDate() == new Date().getDate() && eventDate.getMonth() == new Date().getMonth() && eventDate.getFullYear() == new Date().getFullYear()) {
|
||||
dateDiv.classList.add("today");
|
||||
|
||||
const duration = 15 * 1000,
|
||||
animationEnd = Date.now() + duration,
|
||||
defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
const interval = setInterval(function () {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval);
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
|
||||
// since particles fall down, start a bit higher than random
|
||||
confetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
|
||||
})
|
||||
);
|
||||
confetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
|
||||
})
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
|
||||
const detailsDiv = document.createElement('div');
|
||||
detailsDiv.classList.add("info")
|
||||
const typeDiv = document.createElement('div');
|
||||
typeDiv.classList.add('type');
|
||||
typeDiv.textContent = data.type;
|
||||
|
||||
const titleStrong = document.createElement('strong');
|
||||
titleStrong.textContent = data.title;
|
||||
|
||||
const info = document.createElement('p');
|
||||
info.textContent = data.info;
|
||||
|
||||
|
||||
const timeSmall = document.createElement('small');
|
||||
time = eventDate.getHours().toString().padStart(2, '0') + ':' + eventDate.getMinutes().toString().padStart(2, '0');
|
||||
timeSmall.textContent = `🕒 ${time} Uhr`;
|
||||
if (data.type != "Klausur") {
|
||||
titleStrong.classList.add('rainbow-text');
|
||||
//typeDiv.classList.add('rainbow-text');
|
||||
}
|
||||
|
||||
detailsDiv.appendChild(typeDiv);
|
||||
detailsDiv.appendChild(titleStrong);
|
||||
detailsDiv.appendChild(document.createElement('br'));
|
||||
detailsDiv.appendChild(info);
|
||||
if (data.type == "Klausur") {
|
||||
detailsDiv.appendChild(timeSmall);
|
||||
}
|
||||
|
||||
eventDiv.appendChild(dateDiv);
|
||||
eventDiv.appendChild(detailsDiv);
|
||||
|
||||
eventContainer.appendChild(eventDiv);
|
||||
}
|
||||
|
||||
async function add_event() {
|
||||
|
||||
const userDate = prompt("Bitte gib ein Datum im Format TT.MM.JJJJ ein (z. B. 05.11.2025):");
|
||||
const userTime = prompt("Bitte gib eine Uhrzeit im Format HH:MM ein (z. B. 14:30):");
|
||||
|
||||
const [day, month, year] = userDate.split('.').map(Number);
|
||||
const [hours, minutes] = userTime.split(':').map(Number);
|
||||
|
||||
// Date-Objekt erstellen (Monat ist 0-basiert!)
|
||||
const userDateTime = new Date(year, month - 1, day, hours, minutes);
|
||||
|
||||
// example create data
|
||||
const data = {
|
||||
"date": userDateTime.toISOString(),
|
||||
"title": prompt("Titel?"),
|
||||
"info": prompt("Info?"),
|
||||
"type": prompt("Typ?", "Klausur"),
|
||||
};
|
||||
|
||||
const record = await PB.collection('termine').create(data);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const records = await PB.collection('termine').getList(1, 5, {
|
||||
sort: '+start',
|
||||
filter: `start >= "${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${(new Date().getDate()).toString().padStart(2, '0')} 00:00:00Z"`
|
||||
});
|
||||
|
||||
records.items.forEach(record => render_event(record));
|
||||
})();
|
||||
197
public/index.less
Normal file
197
public/index.less
Normal file
@@ -0,0 +1,197 @@
|
||||
.mainbox {
|
||||
padding: 8px 16px;
|
||||
color: #000000;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.events {
|
||||
|
||||
|
||||
h3 {
|
||||
color: #009688;
|
||||
}
|
||||
|
||||
.event {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 8px;
|
||||
|
||||
.date {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
background: #707070;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-right: 12px;
|
||||
|
||||
&.today {
|
||||
background-color: #222222;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
.type {
|
||||
color: #009688;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.modul {
|
||||
@color_1: white;
|
||||
@color_2: #4f46e5;
|
||||
|
||||
|
||||
#auth-module {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: auto;
|
||||
background: red;
|
||||
color: @color_1;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.auth-box {
|
||||
background: #fff;
|
||||
padding: 2rem;
|
||||
width: 320px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
|
||||
&.shake {
|
||||
animation: shake 0.3s;
|
||||
}
|
||||
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateX(-10px) rotate(-2deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(10px) rotate(2deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(-10px) rotate(-2deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #4f46e5;
|
||||
color: @color_1;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
|
||||
&:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
|
||||
span {
|
||||
color: @color_2;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes rainbow_animation {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 50% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rainbow-text {
|
||||
background: linear-gradient(to right, #6666ff, #0099ff, #00ff00, #ff3399, #6666ff);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: rainbow_animation 6s infinite;
|
||||
background-size: 400% 100%;
|
||||
}
|
||||
264
public/led/index.html
Normal file
264
public/led/index.html
Normal file
@@ -0,0 +1,264 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bild skalieren & anzeigen</title>
|
||||
|
||||
<style>
|
||||
/* Canvas sichtbar skalieren */
|
||||
#outputCanvas {
|
||||
width: 33%;
|
||||
/* Bildschirmbreite */
|
||||
height: auto;
|
||||
/* Höhe automatisch */
|
||||
border: 1px solid black;
|
||||
image-rendering: pixelated;
|
||||
/* WICHTIG: Pixel bleiben scharf */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Bild auswählen, skalieren und im Canvas anzeigen</h2>
|
||||
|
||||
<label>Breite: </label>
|
||||
<input type="number" id="newWidth" value="8"><br>
|
||||
|
||||
<label>Höhe: </label>
|
||||
<input type="number" id="newHeight" value="8"><br><br>
|
||||
<button onclick="myFunction()">Copy text</button>
|
||||
<script>
|
||||
function myFunction() {
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(AnimationData.getPy());
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="file" id="imageInput" accept="image/*,video/*"><br><br>
|
||||
|
||||
<!-- Canvas zur Anzeige -->
|
||||
<canvas id="outputCanvas"></canvas>
|
||||
|
||||
<script>
|
||||
// Pixel-Klasse
|
||||
class Pixel {
|
||||
constructor(x, y, r, g, b, a) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
|
||||
this.a = a;
|
||||
}
|
||||
}
|
||||
|
||||
class Frame {
|
||||
constructor(file) {
|
||||
this.file = URL.createObjectURL(file);
|
||||
this.pixels = [];
|
||||
this.updateSize(8, 8);
|
||||
}
|
||||
|
||||
updateSize(w, h) {
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Interne Pixelgröße des Canvas (NICHT sichtbare Größe)
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
// Bild intern auf dieser Pixelgröße zeichnen
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
|
||||
// Pixel auslesen
|
||||
const imageData = ctx.getImageData(0, 0, w, h);
|
||||
const data = imageData.data;
|
||||
|
||||
this.pixels = [];
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = (y * w + x) * 4;
|
||||
this.pixels.push(new Pixel(x, y, data[i], data[i + 1], data[i + 2], data[i + 3]));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
img.src = this.file;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Animation {
|
||||
constructor(frames, fps = 1) {
|
||||
this.frames = frames;
|
||||
this.fps = fps;
|
||||
this.currentFrameIndex = 0;
|
||||
}
|
||||
|
||||
updateSize(w, h) {
|
||||
this.frames.forEach(f => f.updateSize(w, h));
|
||||
}
|
||||
|
||||
getPy() {
|
||||
|
||||
let string = "";
|
||||
this.frames.forEach((frame, index) => {
|
||||
string += `frame${index} = [`;
|
||||
frame.pixels.forEach((pixel, i) => {
|
||||
const prevPixel = index > 0 ? this.frames[index - 1].pixels[i] : null;
|
||||
if (!prevPixel || prevPixel.r !== pixel.r || prevPixel.g !== pixel.g || prevPixel.b !== pixel.b || prevPixel.a !== pixel.a) {
|
||||
string += `(${i}, ${pixel.r}, ${pixel.g}, ${pixel.b}, ${pixel.a}),`;
|
||||
}
|
||||
});
|
||||
|
||||
string += "]\n";
|
||||
});
|
||||
|
||||
string += "\nFRAMES = [\n";
|
||||
this.frames.forEach((frame, index) => {
|
||||
string += ` frame${index},\n`;
|
||||
});
|
||||
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
getFrame() {
|
||||
return this.frames[this.currentFrameIndex];
|
||||
}
|
||||
|
||||
nextFrame() {
|
||||
this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let AnimationData = new Animation([]);
|
||||
|
||||
|
||||
document.getElementById("imageInput").addEventListener("change", function (event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.type.startsWith("image/")) {
|
||||
AnimationData = new Animation([new Frame(file)], 10);
|
||||
AnimationData.frames[0].updateSize(
|
||||
parseInt(document.getElementById("newWidth").value),
|
||||
parseInt(document.getElementById("newHeight").value)
|
||||
);
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
if (file.type.startsWith("video/")) {
|
||||
|
||||
let targetFPS = 24;
|
||||
let frames = videoDecoder(file, targetFPS);
|
||||
|
||||
AnimationData = new Animation(frames, targetFPS);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(loop);
|
||||
}, 10000);
|
||||
}
|
||||
});
|
||||
|
||||
let accumulator = 0;
|
||||
let lastTime = 0;
|
||||
|
||||
function videoDecoder(file, fps) {
|
||||
let video = document.createElement("video");
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
let frames = [];
|
||||
|
||||
function seek(videoEl, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function cleanup() {
|
||||
videoEl.removeEventListener('seeked', onSeeked);
|
||||
videoEl.removeEventListener('error', onError);
|
||||
}
|
||||
function onSeeked() { cleanup(); resolve(); }
|
||||
function onError(e) { cleanup(); reject(e); }
|
||||
videoEl.addEventListener('seeked', onSeeked);
|
||||
videoEl.addEventListener('error', onError);
|
||||
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
|
||||
});
|
||||
}
|
||||
|
||||
video.addEventListener('loadeddata', async () => {
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 360;
|
||||
|
||||
let delta = 1 / fps;
|
||||
let currentTime = 0;
|
||||
|
||||
for (let currentTime = 0; currentTime < video.duration; currentTime += delta) {
|
||||
await seek(video, currentTime);
|
||||
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
let frame = new Frame(blob);
|
||||
document.body.appendChild(document.createTextNode(`Frame at ${currentTime.toFixed(2)}s`));
|
||||
let img = document.createElement("img");
|
||||
img.src = url;
|
||||
//document.body.appendChild(img);
|
||||
document.body.appendChild(document.createElement("br"));
|
||||
frames.push(frame);
|
||||
}, 'image/png');
|
||||
}
|
||||
});
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
video.load();
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loop(time) {
|
||||
const deltaTime = (time - lastTime) / 1000; // in seconds
|
||||
lastTime = time;
|
||||
accumulator += deltaTime;
|
||||
|
||||
const FRAME_TIME = 1 / AnimationData.fps; // ~0.0167 seconds for 60 FPS
|
||||
// Generate frames only when enough time has passed
|
||||
if (accumulator >= FRAME_TIME) {
|
||||
|
||||
const canvas = document.getElementById("outputCanvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const frame = AnimationData.getFrame();
|
||||
AnimationData.nextFrame();
|
||||
|
||||
|
||||
canvas.width = frame.pixels.reduce((max, p) => Math.max(max, p.x), 0) + 1;
|
||||
canvas.height = frame.pixels.reduce((max, p) => Math.max(max, p.y), 0) + 1;
|
||||
|
||||
for (const pixel of frame.pixels) {
|
||||
ctx.fillStyle = `rgba(${pixel.r}, ${pixel.g}, ${pixel.b}, ${pixel.a / 255})`;
|
||||
ctx.fillRect(pixel.x, pixel.y, 1, 1);
|
||||
}
|
||||
|
||||
accumulator -= FRAME_TIME;
|
||||
}
|
||||
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
199
public/led/v.html
Normal file
199
public/led/v.html
Normal file
@@ -0,0 +1,199 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Video Frame-Stepping mit FPS-Erkennung</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial;
|
||||
padding: 18px;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #ddd;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 10px;
|
||||
color: #444;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Video: Frame für Frame mit FPS-Erkennung</h1>
|
||||
|
||||
<div class="controls">
|
||||
<label>Wähle Datei:
|
||||
<input id="file" type="file" accept="video/*">
|
||||
</label>
|
||||
|
||||
<label>fps:
|
||||
<input id="fps" type="number" step="0.01" value="25" min="1">
|
||||
</label>
|
||||
|
||||
<button id="prev">◀️ Prev Frame</button>
|
||||
<button id="next">Next Frame ▶️</button>
|
||||
<button id="play">Play ▶</button>
|
||||
<button id="pause">Pause ⏸</button>
|
||||
<button id="export">Export Current Frame (PNG)</button>
|
||||
</div>
|
||||
|
||||
<video id="video" controls crossorigin="anonymous" style="display:none"></video>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="info" id="info">
|
||||
Lade ein Video, warte auf Metadaten...
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('file');
|
||||
const video = document.getElementById('video');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const fpsInput = document.getElementById('fps');
|
||||
const prevBtn = document.getElementById('prev');
|
||||
const nextBtn = document.getElementById('next');
|
||||
const playBtn = document.getElementById('play');
|
||||
const pauseBtn = document.getElementById('pause');
|
||||
const exportBtn = document.getElementById('export');
|
||||
const info = document.getElementById('info');
|
||||
|
||||
let fileURL = null;
|
||||
let rafId = null;
|
||||
|
||||
// --- Datei laden ---
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) return;
|
||||
if (fileURL) URL.revokeObjectURL(fileURL);
|
||||
fileURL = URL.createObjectURL(file);
|
||||
video.src = fileURL;
|
||||
video.style.display = 'block';
|
||||
video.load();
|
||||
info.textContent = 'Video geladen. Warte auf Metadaten...';
|
||||
});
|
||||
|
||||
// --- Metadaten geladen ---
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
canvas.width = video.videoWidth || 640;
|
||||
canvas.height = video.videoHeight || 360;
|
||||
seekAndDraw(0).catch(() => { });
|
||||
info.textContent = `Dauer: ${formatSeconds(video.duration)} — Bildgröße: ${canvas.width}×${canvas.height}`;
|
||||
detectFPS();
|
||||
});
|
||||
|
||||
// --- Hilfsfunktionen ---
|
||||
function formatSeconds(s) {
|
||||
if (!isFinite(s)) return '–';
|
||||
const mm = Math.floor(s / 60);
|
||||
const ss = (s % 60).toFixed(2).padStart(5, '0');
|
||||
return `${mm}:${ss}`;
|
||||
}
|
||||
|
||||
function drawCurrentFrame() {
|
||||
try {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
} catch (e) {
|
||||
console.warn('drawImage fehlgeschlagen:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function seek(videoEl, time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
function cleanup() {
|
||||
videoEl.removeEventListener('seeked', onSeeked);
|
||||
videoEl.removeEventListener('error', onError);
|
||||
}
|
||||
function onSeeked() { cleanup(); resolve(); }
|
||||
function onError(e) { cleanup(); reject(e); }
|
||||
videoEl.addEventListener('seeked', onSeeked);
|
||||
videoEl.addEventListener('error', onError);
|
||||
videoEl.currentTime = Math.min(Math.max(time, 0), videoEl.duration || time);
|
||||
});
|
||||
}
|
||||
|
||||
async function seekAndDraw(time) {
|
||||
await seek(video, time);
|
||||
drawCurrentFrame();
|
||||
}
|
||||
|
||||
|
||||
// --- Buttons ---
|
||||
nextBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) + frameDur;
|
||||
await seekAndDraw(Math.min(target + 0.00001, video.duration || target));
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', async () => {
|
||||
const fps = parseFloat(fpsInput.value) || 25;
|
||||
const frameDur = 1 / fps;
|
||||
const target = (video.currentTime || 0) - frameDur;
|
||||
await seekAndDraw(Math.max(target, 0));
|
||||
});
|
||||
|
||||
|
||||
exportBtn.addEventListener('click', () => {
|
||||
drawCurrentFrame();
|
||||
canvas.toBlob(blob => {
|
||||
if (!blob) return;
|
||||
const a = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
a.href = url;
|
||||
a.download = 'frame.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||
}, 'image/png');
|
||||
});
|
||||
|
||||
// --- Seeked Event ---
|
||||
video.addEventListener('seeked', () => { drawCurrentFrame(); });
|
||||
|
||||
// --- Safety Initial Draw ---
|
||||
window.addEventListener('load', () => {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width || 640, canvas.height || 360);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
8
public/lib/confetti.js
Normal file
8
public/lib/confetti.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/lib/css/icons_mobiscroll.ttf
Normal file
BIN
public/lib/css/icons_mobiscroll.ttf
Normal file
Binary file not shown.
BIN
public/lib/css/icons_mobiscroll.woff
Normal file
BIN
public/lib/css/icons_mobiscroll.woff
Normal file
Binary file not shown.
26800
public/lib/css/mobiscroll.javascript.min.css
vendored
Normal file
26800
public/lib/css/mobiscroll.javascript.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
public/lib/less.js
Normal file
11
public/lib/less.js
Normal file
File diff suppressed because one or more lines are too long
1988
public/lib/less.min.js.map
Normal file
1988
public/lib/less.min.js.map
Normal file
File diff suppressed because one or more lines are too long
2
public/lib/pocketbase.umd.js
Normal file
2
public/lib/pocketbase.umd.js
Normal file
File diff suppressed because one or more lines are too long
1
public/lib/pocketbase.umd.js.map
Normal file
1
public/lib/pocketbase.umd.js.map
Normal file
File diff suppressed because one or more lines are too long
60
public/lib/sw.js
Normal file
60
public/lib/sw.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// sw.js
|
||||
self.addEventListener("push", function (event) {
|
||||
const data = event.data ? event.data.json() : {};
|
||||
|
||||
const title = data.title || "Shit da ist was falsch"; // title braucht man, sonst Error
|
||||
|
||||
// Dynamisch Options-Objekt nur mit vorhandenen Werten
|
||||
const options = {};
|
||||
|
||||
if (data.body) options.body = data.body;
|
||||
if (data.icon) options.icon = data.icon;
|
||||
if (data.badge) options.badge = data.badge;
|
||||
if (data.actions) options.actions = data.actions;
|
||||
if (data.requireInteraction !== undefined) options.requireInteraction = data.requireInteraction;
|
||||
if (data.renotify !== undefined) options.renotify = data.renotify;
|
||||
if (data.tag) options.tag = data.tag;
|
||||
if (data.vibrate) options.vibrate = data.vibrate;
|
||||
if (data.url) options.data = { url: data.url };
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener("notificationclick", function (event) {
|
||||
event.notification.close(); // Notification schließen
|
||||
|
||||
// Prüfen, ob eine Action gedrückt wurde
|
||||
if (event.action) {
|
||||
// Dynamische Aktionen vom Server können hier behandelt werden
|
||||
// Beispiel: Wir öffnen die URL, die im Notification-Data-Feld steht
|
||||
if (event.notification.data && event.notification.data.url) {
|
||||
event.waitUntil(
|
||||
clients.openWindow(event.notification.data.url)
|
||||
);
|
||||
}
|
||||
// Optional: Weitere Action-IDs können hier behandelt werden
|
||||
console.log("Action clicked:", event.action);
|
||||
} else {
|
||||
// Notification selbst angeklickt (ohne Button)
|
||||
if (event.notification.data && event.notification.data.url) {
|
||||
event.waitUntil(
|
||||
clients.openWindow(event.notification.data.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
console.log("Service Worker installiert");
|
||||
// Sofort aktivieren, ohne auf alte Version zu warten
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
console.log("Service Worker aktiviert");
|
||||
// Alte Clients übernehmen
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
48
public/mass.html
Normal file
48
public/mass.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
|
||||
<script src="lib/pocketbase.umd.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button onclick="addPic()">Uplaod</button>
|
||||
<script>
|
||||
const PB = new PocketBase();
|
||||
|
||||
PB.autoCancellation(false)
|
||||
|
||||
async function addPic() {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.multiple = true;
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.click();
|
||||
// listen to file input changes and add the selected files to the form data
|
||||
fileInput.addEventListener('change', async function () {
|
||||
|
||||
for (let file of fileInput.files) {
|
||||
const formData = new FormData();
|
||||
// set regular text field
|
||||
formData.append('alt', "demo_mass" || prompt("Bitte eine Bildbeschreibung eingeben:"));
|
||||
formData.append('gewicht', 1);
|
||||
|
||||
formData.append('allowed', true);
|
||||
formData.append('wallpaper', true);
|
||||
|
||||
formData.append('img', file);
|
||||
console.log(file);
|
||||
|
||||
const createdRecord = await PB.collection('images').create(formData);
|
||||
}
|
||||
alert("Bild erfolgreich hochgeladen!");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
971
public/networktester.html
Normal file
971
public/networktester.html
Normal file
@@ -0,0 +1,971 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NETZWERKTESTER™ — Das beste Gadget der Welt</title>
|
||||
<meta name="description" content="Der übertriebenste Netzwerktester-Hype aller Zeiten. Animiert. Neon. Absolut unnötig — und genau deshalb perfekt." />
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--bg:#060611;
|
||||
--bg2:#0b0b1f;
|
||||
--neon:#7CFF6B;
|
||||
--neon2:#7afcff;
|
||||
--hot:#ff4fd8;
|
||||
--warn:#ffd34d;
|
||||
--text:#f2f6ff;
|
||||
--muted:rgba(242,246,255,.72);
|
||||
--card:rgba(255,255,255,.06);
|
||||
--stroke:rgba(255,255,255,.12);
|
||||
--shadow: 0 22px 80px rgba(0,0,0,.65);
|
||||
--radius: 22px;
|
||||
--max: 1180px;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
margin:0;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
||||
background: radial-gradient(1200px 800px at 20% 10%, rgba(122,252,255,.14), transparent 55%),
|
||||
radial-gradient(1100px 900px at 80% 20%, rgba(255,79,216,.12), transparent 55%),
|
||||
radial-gradient(900px 700px at 50% 80%, rgba(124,255,107,.10), transparent 55%),
|
||||
linear-gradient(180deg, var(--bg), var(--bg2));
|
||||
color:var(--text);
|
||||
overflow-x:hidden;
|
||||
}
|
||||
|
||||
/* ======= Animated background grid + lasers ======= */
|
||||
.bg-grid{
|
||||
position:fixed; inset:0;
|
||||
pointer-events:none;
|
||||
opacity:.55;
|
||||
background:
|
||||
linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.06) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
transform: perspective(900px) rotateX(60deg) translateY(-18vh);
|
||||
filter: drop-shadow(0 0 18px rgba(122,252,255,.08));
|
||||
animation: gridFloat 8s ease-in-out infinite;
|
||||
}
|
||||
@keyframes gridFloat{
|
||||
0%,100%{transform: perspective(900px) rotateX(60deg) translateY(-18vh) translateX(0)}
|
||||
50%{transform: perspective(900px) rotateX(60deg) translateY(-16vh) translateX(12px)}
|
||||
}
|
||||
|
||||
.lasers{
|
||||
position:fixed; inset:-30vh -30vw;
|
||||
pointer-events:none;
|
||||
mix-blend-mode: screen;
|
||||
opacity:.65;
|
||||
filter: blur(.2px);
|
||||
background:
|
||||
conic-gradient(from 180deg at 50% 50%,
|
||||
rgba(122,252,255,.0),
|
||||
rgba(122,252,255,.25),
|
||||
rgba(255,79,216,.18),
|
||||
rgba(124,255,107,.18),
|
||||
rgba(255,211,77,.12),
|
||||
rgba(122,252,255,.0));
|
||||
animation: spin 14s linear infinite;
|
||||
}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* ======= Layout ======= */
|
||||
.wrap{max-width:var(--max); margin:0 auto; padding: 22px 18px 90px;}
|
||||
header{
|
||||
position:sticky; top:0; z-index:50;
|
||||
backdrop-filter: blur(12px);
|
||||
background: linear-gradient(180deg, rgba(6,6,17,.78), rgba(6,6,17,.35));
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.nav{
|
||||
max-width:var(--max); margin:0 auto;
|
||||
padding:12px 18px;
|
||||
display:flex; align-items:center; justify-content:space-between; gap:10px;
|
||||
}
|
||||
.brand{
|
||||
display:flex; align-items:center; gap:10px;
|
||||
font-weight:900;
|
||||
letter-spacing:.5px;
|
||||
text-transform:uppercase;
|
||||
user-select:none;
|
||||
}
|
||||
.brand .dot{
|
||||
width:12px;height:12px;border-radius:99px;
|
||||
background: radial-gradient(circle at 30% 30%, #fff, var(--neon2));
|
||||
box-shadow: 0 0 18px rgba(122,252,255,.55), 0 0 34px rgba(255,79,216,.18);
|
||||
animation: pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse{
|
||||
0%,100%{transform:scale(1); opacity:1}
|
||||
50%{transform:scale(1.35); opacity:.85}
|
||||
}
|
||||
.nav a{
|
||||
color:var(--muted);
|
||||
text-decoration:none;
|
||||
font-weight:700;
|
||||
padding:10px 12px;
|
||||
border-radius:12px;
|
||||
transition:.2s;
|
||||
display:none;
|
||||
}
|
||||
.nav a:hover{background:rgba(255,255,255,.06); color:var(--text)}
|
||||
@media (min-width:860px){
|
||||
.nav a{display:inline-block}
|
||||
}
|
||||
|
||||
.cta{
|
||||
display:flex; align-items:center; gap:10px;
|
||||
}
|
||||
.btn{
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.12), rgba(255,255,255,.06));
|
||||
color:var(--text);
|
||||
border-radius:14px;
|
||||
padding:10px 14px;
|
||||
font-weight:850;
|
||||
text-decoration:none;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,.35);
|
||||
transition: transform .15s ease, filter .15s ease;
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
user-select:none;
|
||||
}
|
||||
.btn:hover{transform: translateY(-2px); filter:brightness(1.08)}
|
||||
.btn.primary{
|
||||
border-color: rgba(122,252,255,.25);
|
||||
background: linear-gradient(135deg, rgba(122,252,255,.25), rgba(255,79,216,.16));
|
||||
box-shadow: 0 18px 60px rgba(122,252,255,.14), 0 18px 70px rgba(255,79,216,.10);
|
||||
}
|
||||
.btn .shine{
|
||||
content:"";
|
||||
position:absolute; inset:-30%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.45), transparent);
|
||||
transform: rotate(20deg) translateX(-120%);
|
||||
animation: shine 2.8s ease-in-out infinite;
|
||||
opacity:.8;
|
||||
}
|
||||
@keyframes shine{
|
||||
0%,55%{transform: rotate(20deg) translateX(-140%)}
|
||||
85%,100%{transform: rotate(20deg) translateX(140%)}
|
||||
}
|
||||
|
||||
/* ======= Hero ======= */
|
||||
.hero{
|
||||
padding: 46px 0 16px;
|
||||
display:grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap:18px;
|
||||
position:relative;
|
||||
}
|
||||
@media (min-width:980px){
|
||||
.hero{
|
||||
grid-template-columns: 1.05fr .95fr;
|
||||
align-items:center;
|
||||
padding: 64px 0 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.kicker{
|
||||
display:inline-flex; align-items:center; gap:10px;
|
||||
padding:8px 12px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: rgba(255,255,255,.06);
|
||||
color:var(--muted);
|
||||
font-weight:800;
|
||||
width:fit-content;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,.35);
|
||||
}
|
||||
.badge-live{
|
||||
display:inline-flex; align-items:center; gap:7px;
|
||||
padding:4px 10px;
|
||||
border-radius:999px;
|
||||
font-size:12px;
|
||||
font-weight:900;
|
||||
letter-spacing:.6px;
|
||||
color:#071208;
|
||||
background: linear-gradient(135deg, var(--neon), var(--neon2));
|
||||
box-shadow: 0 0 18px rgba(124,255,107,.22), 0 0 28px rgba(122,252,255,.18);
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.badge-live i{
|
||||
width:8px;height:8px;border-radius:99px;background:#071208;
|
||||
box-shadow: 0 0 0 6px rgba(7,18,8,.15);
|
||||
animation: ping 1.1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ping{
|
||||
0%,100%{transform:scale(1); opacity:1}
|
||||
50%{transform:scale(1.35); opacity:.8}
|
||||
}
|
||||
|
||||
h1{
|
||||
margin: 14px 0 12px;
|
||||
font-size: clamp(36px, 5vw, 62px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -1.2px;
|
||||
}
|
||||
|
||||
.glitch{
|
||||
position:relative;
|
||||
display:inline-block;
|
||||
text-shadow:
|
||||
0 0 14px rgba(122,252,255,.18),
|
||||
0 0 22px rgba(255,79,216,.14);
|
||||
}
|
||||
.glitch::before,.glitch::after{
|
||||
content: attr(data-text);
|
||||
position:absolute; left:0; top:0;
|
||||
opacity:.8;
|
||||
mix-blend-mode: screen;
|
||||
clip-path: inset(0 0 0 0);
|
||||
animation: glitch 2.4s infinite linear alternate-reverse;
|
||||
}
|
||||
.glitch::before{
|
||||
transform: translate(2px,-2px);
|
||||
color: var(--neon2);
|
||||
filter: blur(.2px);
|
||||
}
|
||||
.glitch::after{
|
||||
transform: translate(-2px,2px);
|
||||
color: var(--hot);
|
||||
animation-duration: 2.9s;
|
||||
filter: blur(.25px);
|
||||
}
|
||||
@keyframes glitch{
|
||||
0%{clip-path: inset(0 0 85% 0)}
|
||||
10%{clip-path: inset(12% 0 60% 0)}
|
||||
20%{clip-path: inset(65% 0 10% 0)}
|
||||
30%{clip-path: inset(40% 0 40% 0)}
|
||||
40%{clip-path: inset(80% 0 5% 0)}
|
||||
50%{clip-path: inset(10% 0 75% 0)}
|
||||
60%{clip-path: inset(35% 0 40% 0)}
|
||||
70%{clip-path: inset(5% 0 85% 0)}
|
||||
80%{clip-path: inset(55% 0 20% 0)}
|
||||
90%{clip-path: inset(25% 0 55% 0)}
|
||||
100%{clip-path: inset(70% 0 15% 0)}
|
||||
}
|
||||
|
||||
.sub{
|
||||
color:var(--muted);
|
||||
font-size: clamp(16px, 2.1vw, 20px);
|
||||
line-height:1.5;
|
||||
margin: 0 0 18px;
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.hero-actions{
|
||||
display:flex; flex-wrap:wrap; gap:10px;
|
||||
align-items:center;
|
||||
margin: 18px 0 12px;
|
||||
}
|
||||
|
||||
.stats{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(2, minmax(0,1fr));
|
||||
gap:10px;
|
||||
margin-top: 14px;
|
||||
max-width: 520px;
|
||||
}
|
||||
@media(min-width:520px){
|
||||
.stats{grid-template-columns: repeat(3, minmax(0,1fr));}
|
||||
}
|
||||
.stat{
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
border-radius: 18px;
|
||||
padding: 12px 12px;
|
||||
box-shadow: 0 12px 55px rgba(0,0,0,.35);
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
.stat b{
|
||||
display:block;
|
||||
font-size: 22px;
|
||||
letter-spacing:-.3px;
|
||||
}
|
||||
.stat span{color:var(--muted); font-weight:700; font-size:13px}
|
||||
.stat::after{
|
||||
content:"";
|
||||
position:absolute; inset:-40%;
|
||||
background: radial-gradient(circle at 30% 30%, rgba(122,252,255,.18), transparent 55%),
|
||||
radial-gradient(circle at 80% 70%, rgba(255,79,216,.16), transparent 55%);
|
||||
animation: drift 6s ease-in-out infinite;
|
||||
opacity:.8;
|
||||
}
|
||||
@keyframes drift{
|
||||
0%,100%{transform: translate(0,0) rotate(0deg)}
|
||||
50%{transform: translate(18px,-12px) rotate(8deg)}
|
||||
}
|
||||
.stat > *{position:relative; z-index:1}
|
||||
|
||||
/* ======= Right side device card ======= */
|
||||
.device{
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.09), rgba(255,255,255,.04));
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
box-shadow: var(--shadow);
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
min-height: 420px;
|
||||
}
|
||||
.device::before{
|
||||
content:"";
|
||||
position:absolute; inset:-40%;
|
||||
background: conic-gradient(from 180deg at 50% 50%,
|
||||
rgba(122,252,255,.0),
|
||||
rgba(122,252,255,.22),
|
||||
rgba(255,79,216,.18),
|
||||
rgba(124,255,107,.16),
|
||||
rgba(255,211,77,.10),
|
||||
rgba(122,252,255,.0));
|
||||
animation: spin 10s linear infinite;
|
||||
opacity:.55;
|
||||
}
|
||||
.device-inner{
|
||||
position:relative; z-index:1;
|
||||
height:100%;
|
||||
border-radius: calc(var(--radius) - 10px);
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(0,0,0,.22);
|
||||
overflow:hidden;
|
||||
display:grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
.device-top{
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:12px 12px 10px;
|
||||
gap:10px;
|
||||
border-bottom:1px solid rgba(255,255,255,.10);
|
||||
}
|
||||
.dots{display:flex; gap:7px}
|
||||
.dots i{
|
||||
width:10px;height:10px;border-radius:99px;
|
||||
background: rgba(255,255,255,.16);
|
||||
box-shadow: 0 0 0 6px rgba(255,255,255,.03);
|
||||
}
|
||||
.device-top .mode{
|
||||
font-size:12px; font-weight:900; letter-spacing:.7px;
|
||||
text-transform:uppercase;
|
||||
color:var(--muted);
|
||||
}
|
||||
|
||||
.screen{
|
||||
position:relative;
|
||||
padding: 14px;
|
||||
display:grid;
|
||||
place-items:center;
|
||||
}
|
||||
.screen img{
|
||||
width:100%;
|
||||
height: 280px;
|
||||
object-fit:cover;
|
||||
border-radius: 18px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
filter: saturate(1.15) contrast(1.05);
|
||||
transform: translateZ(0);
|
||||
box-shadow: 0 18px 70px rgba(0,0,0,.55);
|
||||
}
|
||||
.floaters{
|
||||
position:absolute; inset:0;
|
||||
pointer-events:none;
|
||||
overflow:hidden;
|
||||
}
|
||||
.floater{
|
||||
position:absolute;
|
||||
font-size: clamp(16px, 2.6vw, 26px);
|
||||
filter: drop-shadow(0 10px 18px rgba(0,0,0,.55));
|
||||
opacity:.95;
|
||||
animation: floatUp linear infinite;
|
||||
}
|
||||
@keyframes floatUp{
|
||||
from{transform: translateY(40px) rotate(0deg); opacity:0}
|
||||
15%{opacity:1}
|
||||
to{transform: translateY(-380px) rotate(22deg); opacity:0}
|
||||
}
|
||||
|
||||
.device-bottom{
|
||||
padding: 12px;
|
||||
border-top:1px solid rgba(255,255,255,.10);
|
||||
display:flex; gap:10px; align-items:center; justify-content:space-between;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.meter{
|
||||
flex:1;
|
||||
min-width: 210px;
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.05);
|
||||
border-radius: 999px;
|
||||
height: 14px;
|
||||
overflow:hidden;
|
||||
position:relative;
|
||||
}
|
||||
.meter span{
|
||||
display:block; height:100%;
|
||||
width: 20%;
|
||||
background: linear-gradient(90deg, var(--neon2), var(--hot), var(--neon));
|
||||
border-radius: 999px;
|
||||
animation: fill 3.2s ease-in-out infinite;
|
||||
filter: saturate(1.2);
|
||||
}
|
||||
@keyframes fill{
|
||||
0%{width:18%}
|
||||
50%{width:98%}
|
||||
100%{width:30%}
|
||||
}
|
||||
.meter-label{
|
||||
font-weight:900;
|
||||
color:var(--muted);
|
||||
font-size:12px;
|
||||
letter-spacing:.6px;
|
||||
text-transform:uppercase;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
user-select:none;
|
||||
}
|
||||
|
||||
/* ======= Sections ======= */
|
||||
section{padding: 26px 0}
|
||||
.grid{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap:14px;
|
||||
}
|
||||
@media(min-width:980px){
|
||||
.grid{grid-template-columns: 1fr 1fr}
|
||||
}
|
||||
|
||||
.card{
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
box-shadow: 0 18px 70px rgba(0,0,0,.35);
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
.card::before{
|
||||
content:"";
|
||||
position:absolute; inset:-30%;
|
||||
background: radial-gradient(circle at 20% 20%, rgba(122,252,255,.14), transparent 55%),
|
||||
radial-gradient(circle at 70% 70%, rgba(255,79,216,.14), transparent 55%);
|
||||
animation: drift 8s ease-in-out infinite;
|
||||
opacity:.7;
|
||||
}
|
||||
.card > *{position:relative; z-index:1}
|
||||
|
||||
.card h2{
|
||||
margin: 2px 0 10px;
|
||||
font-size: 22px;
|
||||
letter-spacing:-.4px;
|
||||
}
|
||||
.card p{margin: 0; color: var(--muted); line-height:1.55}
|
||||
.bullets{
|
||||
margin: 12px 0 0;
|
||||
padding-left: 0;
|
||||
list-style:none;
|
||||
display:grid;
|
||||
gap:10px;
|
||||
}
|
||||
.bullets li{
|
||||
display:flex; gap:10px;
|
||||
align-items:flex-start;
|
||||
padding: 10px 10px;
|
||||
border-radius: 16px;
|
||||
border:1px solid rgba(255,255,255,.10);
|
||||
background: rgba(0,0,0,.18);
|
||||
}
|
||||
.emoji{
|
||||
width: 34px; height: 34px;
|
||||
display:grid; place-items:center;
|
||||
border-radius: 14px;
|
||||
background: rgba(255,255,255,.07);
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
flex:0 0 auto;
|
||||
}
|
||||
.bullets b{display:block; margin-bottom:2px}
|
||||
.bullets span{color: var(--muted); font-weight:650}
|
||||
|
||||
/* ======= Old tester comparison ======= */
|
||||
.compare{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
align-items:stretch;
|
||||
}
|
||||
@media(min-width:980px){
|
||||
.compare{grid-template-columns: 1fr 1fr}
|
||||
}
|
||||
|
||||
.img-card{
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.05);
|
||||
border-radius: var(--radius);
|
||||
overflow:hidden;
|
||||
box-shadow: var(--shadow);
|
||||
position:relative;
|
||||
}
|
||||
.img-card img{
|
||||
width:100%;
|
||||
height: 320px;
|
||||
object-fit: cover;
|
||||
display:block;
|
||||
filter: contrast(1.05) saturate(1.12);
|
||||
}
|
||||
.img-card .cap{
|
||||
padding: 14px 14px 16px;
|
||||
border-top:1px solid rgba(255,255,255,.10);
|
||||
}
|
||||
.tag{
|
||||
display:inline-flex; align-items:center; gap:8px;
|
||||
font-weight:900;
|
||||
letter-spacing:.5px;
|
||||
text-transform:uppercase;
|
||||
font-size:12px;
|
||||
padding:6px 10px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: rgba(0,0,0,.22);
|
||||
color:var(--muted);
|
||||
}
|
||||
|
||||
/* ======= Testimonials ======= */
|
||||
.quotes{
|
||||
display:grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@media(min-width:860px){
|
||||
.quotes{grid-template-columns: repeat(3, 1fr)}
|
||||
}
|
||||
blockquote{
|
||||
margin:0;
|
||||
padding: 14px;
|
||||
border-radius: var(--radius);
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.06);
|
||||
box-shadow: 0 18px 60px rgba(0,0,0,.28);
|
||||
position:relative;
|
||||
overflow:hidden;
|
||||
}
|
||||
blockquote::before{
|
||||
content:"“";
|
||||
position:absolute;
|
||||
top:-24px; left:10px;
|
||||
font-size: 88px;
|
||||
opacity:.16;
|
||||
}
|
||||
blockquote p{margin:0 0 10px; color:var(--muted); line-height:1.5}
|
||||
blockquote footer{font-weight:900; color:var(--text)}
|
||||
blockquote small{display:block; color:var(--muted); font-weight:750; margin-top:3px}
|
||||
|
||||
/* ======= Footer ======= */
|
||||
footer{
|
||||
margin-top: 26px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid rgba(255,255,255,.10);
|
||||
color: var(--muted);
|
||||
font-weight:700;
|
||||
display:flex; flex-direction:column; gap:8px;
|
||||
}
|
||||
|
||||
/* ======= Scroll reveal ======= */
|
||||
.reveal{
|
||||
opacity:0;
|
||||
transform: translateY(14px) scale(.98);
|
||||
transition: opacity .7s ease, transform .7s ease;
|
||||
}
|
||||
.reveal.on{
|
||||
opacity:1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* ======= Confetti canvas ======= */
|
||||
canvas#confetti{
|
||||
position:fixed; inset:0;
|
||||
pointer-events:none;
|
||||
z-index:999;
|
||||
}
|
||||
|
||||
/* ======= Reduced motion ======= */
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
*{animation:none!important; transition:none!important; scroll-behavior:auto!important}
|
||||
.bg-grid,.lasers{display:none}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="bg-grid" aria-hidden="true"></div>
|
||||
<div class="lasers" aria-hidden="true"></div>
|
||||
<canvas id="confetti"></canvas>
|
||||
|
||||
<header>
|
||||
<div class="nav">
|
||||
<div class="brand">
|
||||
<span class="dot" aria-hidden="true"></span>
|
||||
<span>NETZWERKTESTER™</span>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#compare">Vergleich</a>
|
||||
<a href="#testimonials">Stimmen</a>
|
||||
</nav>
|
||||
<div class="cta">
|
||||
<a class="btn" href="#compare">👀 Showdown</a>
|
||||
<a class="btn primary" id="boostBtn" href="#features">🚀 BOOST <span class="shine" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<div class="kicker">
|
||||
<span class="badge-live"><i></i> Live Hype</span>
|
||||
<span>Übertrieben. Animiert. Absolut notwendig. 😎⚡</span>
|
||||
</div>
|
||||
|
||||
<h1>
|
||||
<span class="glitch" data-text="Der Netzwerktester, den wirklich ALLE brauchen.">Der Netzwerktester, den wirklich ALLE brauchen.</span>
|
||||
<span aria-hidden="true"> 🧪🔌✨</span>
|
||||
</h1>
|
||||
|
||||
<p class="sub">
|
||||
Schluss mit „Warum geht’s nicht?!“ — unser Netzwerktester verwandelt jedes Kabel in eine <b>offiziell zertifizierte
|
||||
Glücksleitung</b>. Er misst nicht nur Durchgang… er misst <b>Aura</b>, <b>Vibes</b> und <b>Respekt</b>. 💚📈
|
||||
</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
<a class="btn primary" href="#features">😤 Ich brauche das jetzt <span class="shine" aria-hidden="true"></span></a>
|
||||
<a class="btn" href="#testimonials">⭐ 100% echte Stimmen*</a>
|
||||
<span class="meter-label">POWER-LEVEL: <b id="powerLevel">9000</b>+</span>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<b><span id="uptime">99.98</span>%</b>
|
||||
<span>Hype-Uptime</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<b><span id="cables">0</span></b>
|
||||
<span>Kabel gerettet</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<b><span id="wow">0</span>×</b>
|
||||
<span>„WOW“ pro Minute</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="device" aria-label="Animiertes Showcase">
|
||||
<div class="device-inner">
|
||||
<div class="device-top">
|
||||
<div class="dots" aria-hidden="true"><i></i><i></i><i></i></div>
|
||||
<div class="mode">ULTRA DIAG MODE 🔥</div>
|
||||
</div>
|
||||
|
||||
<div class="screen">
|
||||
<!-- Bild 1: Person mit Netzwerkkabel -->
|
||||
<img
|
||||
src="https://cdn.pixabay.com/photo/2016/08/15/20/02/compression-pressure-recorder-1596369_1280.jpg"
|
||||
alt="Person hält ein Netzwerkkabel"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="floaters" aria-hidden="true" id="floaters"></div>
|
||||
</div>
|
||||
|
||||
<div class="device-bottom">
|
||||
<div class="meter" aria-hidden="true"><span></span></div>
|
||||
<div class="meter-label">SCAN: <span id="scanText">LINK UP ✅</span> ⚡</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section id="features" class="reveal">
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>Warum ist das Ding so legendär? 🏆</h2>
|
||||
<p>
|
||||
Weil er die Realität debuggt. Du steckst ein Kabel rein — und plötzlich macht die Welt Sinn.
|
||||
(Zumindest das Patchfeld.) 🌍🔧
|
||||
</p>
|
||||
<ul class="bullets">
|
||||
<li>
|
||||
<div class="emoji">🧠</div>
|
||||
<div>
|
||||
<b>IQ-Boost für den Schaltschrank</b>
|
||||
<span>Erkennt Fehler, bevor sie überhaupt passieren. (Gefühlt.)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="emoji">⚡</div>
|
||||
<div>
|
||||
<b>Turbo-Scan in Lichtgeschwindigkeit</b>
|
||||
<span>So schnell, dass dein Blick hinterher ruckelt.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="emoji">🛡️</div>
|
||||
<div>
|
||||
<b>Anti-“Warum geht’s nicht?”-Shield</b>
|
||||
<span>Blockt Ausreden, Drama und Kabel-Karma.</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Für wen? Für ALLE. Wirklich alle. 😤</h2>
|
||||
<p>
|
||||
Schüler, Azubis, Techniker, Netzwerkgötter, IT-Orakel, Menschen mit Kabeln, Menschen ohne Kabeln,
|
||||
sogar Leute, die „LAN“ für eine Stadt halten. 🏙️🔌
|
||||
</p>
|
||||
<ul class="bullets">
|
||||
<li>
|
||||
<div class="emoji">📱</div>
|
||||
<div>
|
||||
<b>Handy & PC: Perfekt sichtbar</b>
|
||||
<span>Responsive Layout, große Buttons, kein Zoomen wie 2012.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="emoji">🎛️</div>
|
||||
<div>
|
||||
<b>Animationen: Maximum Overdrive</b>
|
||||
<span>Neon, Glitch, Laser, Floating Emojis… ja.</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="emoji">🧪</div>
|
||||
<div>
|
||||
<b>Wissenschaftlich übertrieben</b>
|
||||
<span>Messwerte wirken 17% seriöser, wenn sie leuchten.</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="compare" class="reveal">
|
||||
<h2 style="margin:0 0 12px; font-size:28px; letter-spacing:-.5px;">
|
||||
Der epische Vergleich: ALT vs. HYPER-ULTRA 😱
|
||||
</h2>
|
||||
|
||||
<div class="compare">
|
||||
<article class="img-card">
|
||||
<!-- Bild 2: sehr alter Netzwerktester -->
|
||||
<img
|
||||
src="https://cdn.pixabay.com/photo/2013/03/09/02/36/tester-91696_1280.jpg"
|
||||
alt="Sehr alter Netzwerktester"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="cap">
|
||||
<span class="tag">🕰️ Altgerät</span>
|
||||
<h3 style="margin:10px 0 6px;">Der Klassiker: „Geht… irgendwie“</h3>
|
||||
<p style="margin:0; color:var(--muted); line-height:1.5;">
|
||||
Misst Dinge. Manchmal. Mit Charme. Aber ohne Laser. Ohne Glitch. Ohne Drama. (Also langweilig.) 😴
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="img-card">
|
||||
<div style="position:relative">
|
||||
<img
|
||||
src="https://cdn.pixabay.com/photo/2017/12/24/21/08/secret-3037639_1280.jpg"
|
||||
alt="Person mit Netzwerkkabel"
|
||||
loading="lazy"
|
||||
style="filter:saturate(1.3) contrast(1.08);"
|
||||
/>
|
||||
<div style="
|
||||
position:absolute; inset:0;
|
||||
background: radial-gradient(circle at 20% 20%, rgba(122,252,255,.22), transparent 55%),
|
||||
radial-gradient(circle at 70% 70%, rgba(255,79,216,.18), transparent 55%);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events:none;
|
||||
"></div>
|
||||
</div>
|
||||
<div class="cap">
|
||||
<span class="tag">🚀 HYPE-DEVICE</span>
|
||||
<h3 style="margin:10px 0 6px;">NETZWERKTESTER™: „Ich sehe alles.“</h3>
|
||||
<p style="margin:0; color:var(--muted); line-height:1.5;">
|
||||
Findet Fehler. Findet Sinn. Findet deinen verlorenen Schraubendreher (theoretisch). 🧲✨
|
||||
Und sieht dabei gefährlich gut aus. 😎
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="testimonials" class="reveal">
|
||||
<h2 style="margin:0 0 6px; font-size:28px; letter-spacing:-.5px;">
|
||||
100% echte Stimmen* ⭐
|
||||
</h2>
|
||||
<p style="margin:0 0 10px; color:var(--muted); font-weight:700;">
|
||||
*Echt im Sinne von: Niemand hat widersprochen. 🤝😇
|
||||
</p>
|
||||
|
||||
<div class="quotes">
|
||||
<blockquote>
|
||||
<p>„Ich hab ihn einmal eingeschaltet und plötzlich war mein Leben geordnet. Sogar die Patchkabel.“</p>
|
||||
<footer>Max, <small>zertifizierter Kabel-Flüsterer 🧙♂️</small></footer>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>„Er sagt ‘LINK UP’ mit so viel Selbstbewusstsein, ich fühl mich direkt kompetenter.“</p>
|
||||
<footer>Sara, <small>Fehlerfinderin auf Koffein ☕</small></footer>
|
||||
</blockquote>
|
||||
<blockquote>
|
||||
<p>„Ich wollte keinen. Jetzt hab ich drei. Einen fürs Bett, einen fürs Auto, einen fürs Herz.“</p>
|
||||
<footer>Ali, <small>LAN-Romantiker 💘</small></footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div>© <span id="year"></span> Netzwerktester™ — „Wenn’s leuchtet, ist’s besser.“ ✨</div>
|
||||
<div style="opacity:.85"><a href="https://www.fsae41.de">HOME</a></div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ===== Scroll reveal =====
|
||||
const revealEls = document.querySelectorAll('.reveal');
|
||||
const io = new IntersectionObserver((entries)=>{
|
||||
for (const e of entries){
|
||||
if (e.isIntersecting) e.target.classList.add('on');
|
||||
}
|
||||
}, {threshold: 0.12});
|
||||
revealEls.forEach(el => io.observe(el));
|
||||
|
||||
// ===== Floating emojis in the "device screen" =====
|
||||
const floaters = document.getElementById('floaters');
|
||||
const EMOJIS = ["🔌","⚡","✨","📶","🧪","😎","🟩","💥","🧠","🛠️","🌈","🚀"];
|
||||
function spawnFloater(){
|
||||
const el = document.createElement('div');
|
||||
el.className = 'floater';
|
||||
el.textContent = EMOJIS[Math.floor(Math.random()*EMOJIS.length)];
|
||||
el.style.left = Math.random()*100 + "%";
|
||||
el.style.bottom = (-10 - Math.random()*10) + "px";
|
||||
el.style.animationDuration = (2.2 + Math.random()*2.8) + "s";
|
||||
el.style.animationDelay = (Math.random()*0.2) + "s";
|
||||
el.style.transform = `translateY(40px) rotate(${(Math.random()*18-9).toFixed(1)}deg)`;
|
||||
floaters.appendChild(el);
|
||||
setTimeout(()=> el.remove(), 5200);
|
||||
}
|
||||
setInterval(spawnFloater, 180);
|
||||
|
||||
// ===== Fake live scan text =====
|
||||
const scanText = document.getElementById('scanText');
|
||||
const scanPhrases = [
|
||||
"LINK UP ✅", "PAIR OK ✅", "GIGABIT DREAM ✅", "VIBES: STABIL ✅",
|
||||
"PATCHKABEL: GLÜCKLICH ✅", "KARMA: GEROUTET ✅", "FEHLER: GEFUNDEN 🎯"
|
||||
];
|
||||
setInterval(()=>{
|
||||
scanText.textContent = scanPhrases[Math.floor(Math.random()*scanPhrases.length)];
|
||||
}, 1300);
|
||||
|
||||
// ===== Counters =====
|
||||
const yearEl = document.getElementById('year');
|
||||
yearEl.textContent = new Date().getFullYear();
|
||||
|
||||
const uptimeEl = document.getElementById('uptime');
|
||||
const cablesEl = document.getElementById('cables');
|
||||
const wowEl = document.getElementById('wow');
|
||||
const powerLevelEl = document.getElementById('powerLevel');
|
||||
|
||||
let cables = 0;
|
||||
let wow = 0;
|
||||
let power = 9001;
|
||||
|
||||
function tick(){
|
||||
cables += Math.floor(1 + Math.random()*6);
|
||||
wow += Math.floor(1 + Math.random()*4);
|
||||
power += Math.floor(6 + Math.random()*18);
|
||||
|
||||
cablesEl.textContent = cables.toLocaleString('de-DE');
|
||||
wowEl.textContent = wow.toLocaleString('de-DE');
|
||||
powerLevelEl.textContent = power.toLocaleString('de-DE');
|
||||
|
||||
// uptime wiggle
|
||||
const base = 99.90;
|
||||
const v = (base + Math.random()*0.09).toFixed(2);
|
||||
uptimeEl.textContent = v;
|
||||
}
|
||||
setInterval(tick, 900);
|
||||
tick();
|
||||
|
||||
// ===== Confetti "BOOST" (simple canvas particles) =====
|
||||
const canvas = document.getElementById('confetti');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H;
|
||||
function resize(){
|
||||
W = canvas.width = window.innerWidth * devicePixelRatio;
|
||||
H = canvas.height = window.innerHeight * devicePixelRatio;
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
let particles = [];
|
||||
function burst(){
|
||||
const count = 170;
|
||||
for(let i=0;i<count;i++){
|
||||
particles.push({
|
||||
x: (Math.random()*window.innerWidth) * devicePixelRatio,
|
||||
y: (-10 - Math.random()*60) * devicePixelRatio,
|
||||
vx: (-1.2 + Math.random()*2.4) * devicePixelRatio,
|
||||
vy: (2.0 + Math.random()*4.4) * devicePixelRatio,
|
||||
r: (2 + Math.random()*4) * devicePixelRatio,
|
||||
rot: Math.random()*Math.PI*2,
|
||||
vr: (-0.18 + Math.random()*0.36),
|
||||
life: 0,
|
||||
max: 160 + Math.random()*120
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function draw(){
|
||||
ctx.clearRect(0,0,W,H);
|
||||
for(const p of particles){
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy += 0.02 * devicePixelRatio; // gravity
|
||||
p.rot += p.vr;
|
||||
p.life++;
|
||||
|
||||
const alpha = Math.max(0, 1 - p.life / p.max);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha * 0.9;
|
||||
ctx.translate(p.x, p.y);
|
||||
ctx.rotate(p.rot);
|
||||
|
||||
// No fixed colors: use random-ish neon via hue
|
||||
const hue = (p.life*2 + p.x/W*360) % 360;
|
||||
ctx.fillStyle = `hsla(${hue}, 95%, 60%, 1)`;
|
||||
ctx.fillRect(-p.r, -p.r, p.r*2.2, p.r*1.2);
|
||||
ctx.restore();
|
||||
}
|
||||
particles = particles.filter(p => p.life < p.max && p.y < H + 120);
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
draw();
|
||||
|
||||
document.getElementById('boostBtn').addEventListener('click', (e)=>{
|
||||
// allow anchor scroll, but also boost visuals
|
||||
burst();
|
||||
// micro screen shake
|
||||
document.body.animate([
|
||||
{ transform: 'translate(0,0)' },
|
||||
{ transform: 'translate(2px,-2px)' },
|
||||
{ transform: 'translate(-2px,2px)' },
|
||||
{ transform: 'translate(1px,1px)' },
|
||||
{ transform: 'translate(0,0)' }
|
||||
], { duration: 320, iterations: 1 });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
185
public/old/countdown.html
Normal file
185
public/old/countdown.html
Normal file
@@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Countdown/Countup Timer</title>
|
||||
<!-- Tab-Icon hinzufügen -->
|
||||
<link rel="icon" href="https://cdn-icons-png.flaticon.com/512/8730/8730547.png" type="image/x-icon">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#main-heading {
|
||||
color: red;
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 30px;
|
||||
font-weight: 900;
|
||||
background-color: #90EE90;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#target-info {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
font-size: 2rem;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#totals {
|
||||
margin-top: 20px;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main-heading">geht bald los...</div>
|
||||
<div id="target-info">Bis zum 28.08.2025 um 17:30:00 Uhr sind es noch:</div>
|
||||
<div id="countdown">
|
||||
1d 0h 38m 26s 801ms
|
||||
</div>
|
||||
<div id="totals">
|
||||
Jahre: 0.00 |
|
||||
Wochen: 0 |
|
||||
Tage: 1 |
|
||||
Stunden: 24 |
|
||||
Sekunden: 88.706
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to get query parameter from URL
|
||||
function getQueryParam(param) {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(param);
|
||||
}
|
||||
|
||||
// Function to pad numbers with leading zeros
|
||||
function padZero(num, places) {
|
||||
return num.toString().padStart(places, '0');
|
||||
}
|
||||
|
||||
// Function to format number with thousand separators
|
||||
function formatNumber(num) {
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
}
|
||||
|
||||
// Function to format date for display
|
||||
function formatDate(date) {
|
||||
return `${padZero(date.getDate(), 2)}.${padZero(date.getMonth() + 1, 2)}.${date.getFullYear()} um ${padZero(date.getHours(), 2)}:${padZero(date.getMinutes(), 2)}:${padZero(date.getSeconds(), 2)}`;
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
const targetDateStr = getQueryParam('target');
|
||||
const headingText = getQueryParam('heading');
|
||||
|
||||
// Select elements
|
||||
const mainHeadingElement = document.getElementById('main-heading');
|
||||
const targetInfoElement = document.getElementById('target-info');
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
const totalsElement = document.getElementById('totals');
|
||||
|
||||
// Set heading if provided
|
||||
if (headingText) {
|
||||
mainHeadingElement.textContent = decodeURIComponent(headingText);
|
||||
} else {
|
||||
mainHeadingElement.style.display = 'none';
|
||||
}
|
||||
|
||||
// Handle different scenarios
|
||||
if (!targetDateStr) {
|
||||
// No target specified
|
||||
targetInfoElement.innerHTML = "Ja, worauf warten wir?";
|
||||
countdownElement.innerHTML = "";
|
||||
totalsElement.innerHTML = "";
|
||||
} else {
|
||||
try {
|
||||
// Parse the target date
|
||||
const targetDate = new Date(targetDateStr);
|
||||
|
||||
// Validate the date
|
||||
if (isNaN(targetDate.getTime())) {
|
||||
targetInfoElement.innerHTML = "hmm, da stimmt was nicht!";
|
||||
countdownElement.innerHTML = "";
|
||||
totalsElement.innerHTML = "";
|
||||
} else {
|
||||
// Determine if date is in past or future
|
||||
const now = new Date().getTime();
|
||||
const target = targetDate.getTime();
|
||||
let isInPast = target < now;
|
||||
|
||||
// Countdown/Countup function
|
||||
function updateTimer() {
|
||||
const now = new Date().getTime();
|
||||
const distance = Math.abs(target - now);
|
||||
isInPast = target < now;
|
||||
|
||||
// Set target info text based on past/future
|
||||
const targetInfoText = isInPast
|
||||
? `Seit dem ${formatDate(targetDate)} Uhr sind es schon:`
|
||||
: `Bis zum ${formatDate(targetDate)} Uhr sind es noch:`;
|
||||
targetInfoElement.innerHTML = targetInfoText;
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
const milliseconds = Math.floor(distance % 1000);
|
||||
|
||||
// Calculate total values
|
||||
const totalDays = days;
|
||||
const totalWeeks = Math.floor(totalDays / 7);
|
||||
const totalHours = totalDays * 24 + hours;
|
||||
const totalSeconds = totalDays * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds;
|
||||
const totalYears = (totalDays / 365).toFixed(2); // Calculate total years to 2 decimal places
|
||||
|
||||
// Display countdown/countup
|
||||
countdownElement.innerHTML = `
|
||||
${days}d ${hours}h ${minutes}m ${seconds}s ${padZero(milliseconds, 3)}ms
|
||||
`;
|
||||
|
||||
// Display totals with thousand separators
|
||||
totalsElement.innerHTML = `
|
||||
Jahre: ${totalYears} |
|
||||
Wochen: ${formatNumber(totalWeeks)} |
|
||||
Tage: ${formatNumber(totalDays)} |
|
||||
Stunden: ${formatNumber(totalHours)} |
|
||||
Sekunden: ${formatNumber(totalSeconds)}
|
||||
`;
|
||||
}
|
||||
|
||||
// Update timer every 10 milliseconds
|
||||
setInterval(updateTimer, 10);
|
||||
updateTimer(); // Initial call
|
||||
}
|
||||
} catch (error) {
|
||||
targetInfoElement.innerHTML = "hmm, da stimmt was nicht!";
|
||||
countdownElement.innerHTML = "";
|
||||
totalsElement.innerHTML = "";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
234
public/old/index.html
Normal file
234
public/old/index.html
Normal file
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="robots" content="nofollow">
|
||||
|
||||
<link rel="icon" href="https://lifab.de/favicon_16x_32x_48x_v1.ico" type="image/x-icon">
|
||||
<script defer src="https://analytics.fsae41.de/script.js" data-website-id="257da02e-d678-47b6-b036-e3bdabaf1405"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Unterrichtszeit Countdown</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #1e1e1e;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
width: 90vw;
|
||||
height: 60vh;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.wheeler {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<script src="/lib/pocketbase.umd.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="clock">14:51:32</div>
|
||||
<div id="countdown-container">
|
||||
<iframe id="countdown" src="countdown_old.html"></iframe>
|
||||
</div>
|
||||
<canvas id="myChart" style="display:none;width:100%;max-width:700px"></canvas>
|
||||
<div class="wheeler">
|
||||
<img id="image" src="Wheeler.webp" alt="Wheeler">
|
||||
</div>
|
||||
<div style="padding: 5px; background-color: white;">
|
||||
<a href="https://fsae41.de/ausbildung_quiz.html">AEVO-Held</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let PB = new PocketBase();
|
||||
|
||||
let lastURL = "";
|
||||
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
document.getElementById('clock').innerText = now.toLocaleTimeString('de-DE');
|
||||
}
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
function getCountdownURL() {
|
||||
const now = new Date();
|
||||
const day = now.getDay();
|
||||
const current = now.getTime();
|
||||
const todayStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
||||
|
||||
const schedule = {
|
||||
1: [ // Montag
|
||||
{ start: "17:30", end: "19:00", label: "1. Stunde" },
|
||||
{ start: "19:00", end: "19:15", label: "Pause" },
|
||||
{ start: "19:15", end: "20:45", label: "2. Stunde" },
|
||||
],
|
||||
2: [ // Dienstag
|
||||
{ start: "17:00", end: "18:20", label: "1. Stunde" },
|
||||
{ start: "18:20", end: "18:30", label: "Pause" },
|
||||
{ start: "18:30", end: "19:50", label: "2. Stunde" },
|
||||
{ start: "19:50", end: "20:00", label: "Pause" },
|
||||
{ start: "20:00", end: "21:20", label: "3. Stunde" },
|
||||
],
|
||||
4: [ // Donnerstag
|
||||
{ start: "17:30", end: "19:00", label: "1. Stunde" },
|
||||
{ start: "19:00", end: "19:15", label: "Pause" },
|
||||
{ start: "19:15", end: "20:45", label: "2. Stunde" },
|
||||
]
|
||||
};
|
||||
|
||||
const todaySchedule = schedule[day] || [];
|
||||
|
||||
for (const block of todaySchedule) {
|
||||
const [startH, startM] = block.start.split(":").map(Number);
|
||||
const [endH, endM] = block.end.split(":").map(Number);
|
||||
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), startH, startM);
|
||||
const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), endH, endM);
|
||||
|
||||
if (current >= startTime.getTime() && current < endTime.getTime()) {
|
||||
return `https://fsae41.de/old/countdown.html?target=${todayStr}T${pad(endH)}:${pad(endM)}:00&heading=${encodeURIComponent(block.label)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Kein aktueller Block aktiv: zeige nächstes Ereignis
|
||||
const futureTimes = [];
|
||||
for (let offset = 0; offset < 7; offset++) {
|
||||
const checkDay = (day + offset) % 7;
|
||||
const checkDate = new Date(now);
|
||||
checkDate.setDate(now.getDate() + offset);
|
||||
const dateStr = `${checkDate.getFullYear()}-${pad(checkDate.getMonth() + 1)}-${pad(checkDate.getDate())}`;
|
||||
if (!schedule[checkDay]) continue;
|
||||
|
||||
for (const block of schedule[checkDay]) {
|
||||
const [startH, startM] = block.start.split(":").map(Number);
|
||||
|
||||
const blockStart = new Date(checkDate.getFullYear(), checkDate.getMonth(), checkDate.getDate(), startH, startM);
|
||||
if (blockStart.getTime() > current) {
|
||||
futureTimes.push({ time: blockStart, label: "geht bald los..." });
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (futureTimes.length > 0) break;
|
||||
}
|
||||
|
||||
|
||||
if (futureTimes.length > 0) {
|
||||
const next = futureTimes[0];
|
||||
let x = next.time.getHours(); // Korrigieren der Zeitumstellung
|
||||
next.time.setHours(x + 2);
|
||||
return `https://fsae41.de/old/countdown.html?target=${next.time.toISOString().split('.')[0]}&heading=geht%20bald%20los...`;
|
||||
}
|
||||
|
||||
return `https://fsae41.de/old/countdown.html?target=2099-01-01T00:00:00&heading=Fehler`;
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
const url = getCountdownURL();
|
||||
if (url !== lastURL) {
|
||||
location.reload(); // Ganze Seite neu laden, wenn URL sich ändert
|
||||
}
|
||||
}
|
||||
|
||||
function setInitialCountdown() {
|
||||
const url = getCountdownURL();
|
||||
lastURL = url;
|
||||
document.getElementById('countdown').src = url;
|
||||
}
|
||||
|
||||
async function addView() {
|
||||
const record = await PB.collection('views').create({ device: navigator.userAgent });
|
||||
|
||||
fetch('https://fsae41.de/views').then(response => response.json()).then(data => {
|
||||
|
||||
let time = [0];
|
||||
let counts = [0];
|
||||
|
||||
for (let i = data.list.length - 1; i > 0; i--) {
|
||||
let x = data.list[i];
|
||||
time.push(i);
|
||||
counts.push(x.count);
|
||||
}
|
||||
|
||||
new Chart("myChart", {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: time,
|
||||
datasets: [{
|
||||
backgroundColor: "rgba(0,0,255,1.0)",
|
||||
borderColor: "rgba(0,0,255,0.1)",
|
||||
data: counts
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: "Zeitpunkte" } },
|
||||
y: { title: { display: true, text: "Views" }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addView();
|
||||
updateClock();
|
||||
setInitialCountdown();
|
||||
setInterval(updateClock, 1000);
|
||||
setInterval(updateCountdown, 10000); // alle 10 Sekunden prüfen
|
||||
setInterval(addView, 30000); // alle 30 Sekunden prüfen
|
||||
|
||||
|
||||
// Subscribe to changes in any popups record
|
||||
PB.collection('popups').subscribe('*', function (e) {
|
||||
if (e.action === 'create') {
|
||||
if (e.record.image_url) {
|
||||
document.getElementById("image").src = e.record.image_url;
|
||||
document.getElementById("image").style.display = "block";
|
||||
setTimeout(() => {
|
||||
document.getElementById("image").style.display = "none";
|
||||
}, 5000); // Nach 15 Sekunden zurücksetzen
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
public/schule.ico
Normal file
BIN
public/schule.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
public/static/nico.png
Normal file
BIN
public/static/nico.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
BIN
public/static/qr-code_fsae41.png
Normal file
BIN
public/static/qr-code_fsae41.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
60
public/sw.js
Normal file
60
public/sw.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// sw.js
|
||||
self.addEventListener("push", function (event) {
|
||||
const data = event.data ? event.data.json() : {};
|
||||
|
||||
const title = data.title || "Shit da ist was falsch"; // title braucht man, sonst Error
|
||||
|
||||
// Dynamisch Options-Objekt nur mit vorhandenen Werten
|
||||
const options = {};
|
||||
|
||||
if (data.body) options.body = data.body;
|
||||
if (data.icon) options.icon = data.icon;
|
||||
if (data.badge) options.badge = data.badge;
|
||||
if (data.actions) options.actions = data.actions;
|
||||
if (data.requireInteraction !== undefined) options.requireInteraction = data.requireInteraction;
|
||||
if (data.renotify !== undefined) options.renotify = data.renotify;
|
||||
if (data.tag) options.tag = data.tag;
|
||||
if (data.vibrate) options.vibrate = data.vibrate;
|
||||
if (data.url) options.data = { url: data.url };
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener("notificationclick", function (event) {
|
||||
event.notification.close(); // Notification schließen
|
||||
|
||||
// Prüfen, ob eine Action gedrückt wurde
|
||||
if (event.action) {
|
||||
// Dynamische Aktionen vom Server können hier behandelt werden
|
||||
// Beispiel: Wir öffnen die URL, die im Notification-Data-Feld steht
|
||||
if (event.notification.data && event.notification.data.url) {
|
||||
event.waitUntil(
|
||||
clients.openWindow(event.notification.data.url)
|
||||
);
|
||||
}
|
||||
// Optional: Weitere Action-IDs können hier behandelt werden
|
||||
console.log("Action clicked:", event.action);
|
||||
} else {
|
||||
// Notification selbst angeklickt (ohne Button)
|
||||
if (event.notification.data && event.notification.data.url) {
|
||||
event.waitUntil(
|
||||
clients.openWindow(event.notification.data.url)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
console.log("Service Worker installiert");
|
||||
// Sofort aktivieren, ohne auf alte Version zu warten
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
console.log("Service Worker aktiviert");
|
||||
// Alte Clients übernehmen
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
313
public/test.html
Normal file
313
public/test.html
Normal file
@@ -0,0 +1,313 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
:root {
|
||||
--numDays: 5;
|
||||
--numHours: 7;
|
||||
--timeHeight: 60px;
|
||||
--calBgColor: #fff1f8;
|
||||
--eventBorderColor: #f2d3d8;
|
||||
--eventColor1: #ffd6d1;
|
||||
--eventColor2: #fafaa3;
|
||||
--eventColor3: #e2f8ff;
|
||||
--eventColor4: #d1ffe6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: auto 1fr;
|
||||
margin: 2rem;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(var(--numHours), var(--timeHeight));
|
||||
}
|
||||
|
||||
.days {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
gap: 5px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.events {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(var(--numHours), var(--timeHeight));
|
||||
border-radius: 5px;
|
||||
background: var(--calBgColor);
|
||||
}
|
||||
|
||||
.start-10 {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.start-12 {
|
||||
grid-row-start: 4;
|
||||
}
|
||||
|
||||
.start-1 {
|
||||
grid-row-start: 5;
|
||||
}
|
||||
|
||||
.start-2 {
|
||||
grid-row-start: 6;
|
||||
}
|
||||
|
||||
.end-12 {
|
||||
grid-row-end: 4;
|
||||
}
|
||||
|
||||
.end-1 {
|
||||
grid-row-end: 5;
|
||||
}
|
||||
|
||||
.end-3 {
|
||||
grid-row-end: 7;
|
||||
}
|
||||
|
||||
.end-4 {
|
||||
grid-row-end: 8;
|
||||
}
|
||||
|
||||
.end-5 {
|
||||
grid-row-end: 9;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.event {
|
||||
border: 1px solid var(--eventBorderColor);
|
||||
border-radius: 5px;
|
||||
padding: 0.5rem;
|
||||
margin: 0 0.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.space,
|
||||
.date {
|
||||
height: 60px
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.corp-fi {
|
||||
background: var(--eventColor1);
|
||||
}
|
||||
|
||||
.ent-law {
|
||||
background: var(--eventColor2);
|
||||
}
|
||||
|
||||
.writing {
|
||||
background: var(--eventColor3);
|
||||
}
|
||||
|
||||
.securities {
|
||||
background: var(--eventColor4);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.date-num {
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.date-day {
|
||||
display: inline;
|
||||
font-size: 3rem;
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="calendar">
|
||||
<div class="days">
|
||||
<div class="day mon">
|
||||
<div class="date">
|
||||
<p class="date-num">9</p>
|
||||
<p class="date-day">Mon</p>
|
||||
</div>
|
||||
<div class="events">
|
||||
<div class="event start-2 end-5 securities">
|
||||
<p class="title">Securities Regulation</p>
|
||||
<p class="time">2:00 - 5:00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day tues">
|
||||
<div class="date">
|
||||
<p class="date-num">12</p>
|
||||
<p class="date-day">Tues</p>
|
||||
</div>
|
||||
<div class="events">
|
||||
<div class="event start-10 end-12 corp-fi">
|
||||
<p class="title">Corporate Finance</p>
|
||||
<p class="time">10:00 - 12:00</p>
|
||||
</div>
|
||||
<div class="event start-1 end-4 ent-law">
|
||||
<p class="title">Entertainment Law</p>
|
||||
<p class="time">1PM - 4PM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day wed">
|
||||
<div class="date">
|
||||
<p class="date-num">11</p>
|
||||
<p class="date-day">Wed</p>
|
||||
</div>
|
||||
<div class="events">
|
||||
<div class="event start-12 end-1 writing">
|
||||
<p class="title">Writing Seminar</p>
|
||||
<p class="time">11:00 - 12:00</p>
|
||||
</div>
|
||||
<div class="event start-2 end-5 securities">
|
||||
<p class="title">Securities Regulation</p>
|
||||
<p class="time">2:00 - 5:00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day thurs">
|
||||
<div class="date">
|
||||
<p class="date-num">12</p>
|
||||
<p class="date-day">Thurs</p>
|
||||
</div>
|
||||
<div class="events">
|
||||
<div class="event start-10 end-12 corp-fi">
|
||||
<p class="title">Corporate Finance</p>
|
||||
<p class="time">10:00 - 12:00</p>
|
||||
</div>
|
||||
<div class="event start-1 end-4 ent-law">
|
||||
<p class="title">Entertainment Law</p>
|
||||
<p class="time">1PM - 4PM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="day fri">
|
||||
<div class="date">
|
||||
<p class="date-num">13</p>
|
||||
<p class="date-day">Fri</p>
|
||||
</div>
|
||||
<div class="events">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
class Calander {
|
||||
|
||||
#timeHeight = 60;
|
||||
#timeSlice = 1;
|
||||
|
||||
constructor(parent) {
|
||||
this.parent = document.querySelector(parent);
|
||||
|
||||
this.elements = {
|
||||
timeline: undefined,
|
||||
days: undefined,
|
||||
events: {},
|
||||
}
|
||||
|
||||
this.createTimeline();
|
||||
this.createDays();
|
||||
|
||||
this.parent.appendChild(this.elements.timeline);
|
||||
this.parent.appendChild(this.elements.days);
|
||||
}
|
||||
|
||||
createElement(type, content) {
|
||||
let el = document.createElement(type);
|
||||
if (content) el.textContent = content;
|
||||
return el;
|
||||
}
|
||||
|
||||
createTimeline(von = 16, bis = 22) {
|
||||
let timeline = this.createElement('div');
|
||||
|
||||
timeline.style.display = 'grid';
|
||||
timeline.style.gridTemplateRows = `repeat(${bis - von + 2}, ${this.#timeHeight}px)`;
|
||||
|
||||
console.log(timeline);
|
||||
|
||||
let spacer = this.createElement('div');
|
||||
timeline.appendChild(spacer);
|
||||
|
||||
for (let i = von; i <= bis; i++) {
|
||||
let timeMarker = this.createElement('div', `${i}:00`);
|
||||
timeline.appendChild(timeMarker);
|
||||
}
|
||||
|
||||
this.elements.timeline = timeline;
|
||||
}
|
||||
|
||||
createDays() {
|
||||
let days = this.createElement('div');
|
||||
let dayNames = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri'];
|
||||
|
||||
days.style.display = 'grid';
|
||||
days.style.gridColumn = '2';
|
||||
days.style.gap = '5px';
|
||||
days.style.gridTemplateColumns = 'repeat(auto-fit, minmax(150px, 1fr))';
|
||||
|
||||
dayNames.forEach((dayName, i) => {
|
||||
let date = this.createElement('div');
|
||||
date.style.display = 'flex';
|
||||
date.style.gap = '1em';
|
||||
|
||||
let daytime = new Date();
|
||||
daytime.setDate(daytime.getDate() + (daytime.getDay() === 0 ? -6 : 1 - daytime.getDay()));
|
||||
let num = this.createElement('p', daytime.getDate() + i);
|
||||
num.style.fontSize = '3rem';
|
||||
num.style.fontWeight = '600';
|
||||
num.style.display = 'inline';
|
||||
date.appendChild(num);
|
||||
|
||||
let day = this.createElement('p', dayName);
|
||||
day.style.fontSize = '3rem';
|
||||
day.style.fontWeight = '100';
|
||||
day.style.display = 'inline';
|
||||
date.appendChild(day);
|
||||
|
||||
days.appendChild(date);
|
||||
let events = this.createElement('div');
|
||||
this.elements.events.dayName = events;
|
||||
days.appendChild(events);
|
||||
});
|
||||
|
||||
|
||||
this.elements.days = days;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let cal = new Calander('.calendar');
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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