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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="de">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>WebSite</title>
|
<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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</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