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:
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));
|
||||
})();
|
||||
Reference in New Issue
Block a user