- 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.
339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
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));
|
|
})(); |