fix(ui): clickable update icon and build-time unit tests (v1.2.3)

This commit is contained in:
2026-02-16 22:13:37 +01:00
parent 876da1a2de
commit f9b29254f9
9 changed files with 428 additions and 273 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

17
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1667,7 +1667,7 @@ body {
<div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="header-left">
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.2</small></h1>
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.3</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div>
</div>
</div>
@@ -3005,7 +3005,7 @@ body {
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = 'v1.2.2';
const CurrentVersion = 'v1.2.3';
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
@@ -3051,161 +3051,170 @@ body {
document.body.appendChild(updateBanner);
updateBanner.querySelector('.close-update').addEventListener('click', () => updateBanner.remove());
// Highlight Header Icon
// Highlight Header Icon -> Make Clickable
const lastUpdatedIcon = document.querySelector('.material-icons-round.logo-icon');
if (lastUpdatedIcon) {
lastUpdatedIcon.style.color = 'var(--accent-color)';
lastUpdatedIcon.parentElement.title = `Update verfügbar: ${remoteVersion}`;
const updateLink = document.createElement('a');
updateLink.href = InstallerUrl;
updateLink.target = '_blank';
updateLink.className = 'material-icons-round logo-icon update-pulse';
updateLink.style.color = 'var(--accent-color)';
updateLink.style.textDecoration = 'none';
updateLink.style.cursor = 'pointer';
updateLink.title = `Update verfügbar: ${remoteVersion}`;
updateLink.textContent = 'system_update'; // Change icon to update icon
lastUpdatedIcon.replaceWith(updateLink);
}
}
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
// === Order Countdown ===
function updateCountdown() {
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
if (currentDay === 0 || currentDay === 6) {
removeCountdown();
return;
}
const todayStr = now.toISOString().split('T')[0];
// 1. Check if we already ordered for today
let hasOrder = false;
// Optimization: Check orderMap for today's date
// Keys are "YYYY-MM-DD_ArticleID"
for (const key of orderMap.keys()) {
if (key.startsWith(todayStr)) {
hasOrder = true;
break;
}
}
// === Order Countdown ===
function updateCountdown() {
const now = new Date();
const currentDay = now.getDay();
// Skip weekends (0=Sun, 6=Sat)
if (currentDay === 0 || currentDay === 6) {
removeCountdown();
return;
}
if (hasOrder) {
removeCountdown();
return;
}
const todayStr = now.toISOString().split('T')[0];
// 2. Calculate time to cutoff (10:00 AM)
const cutoff = new Date();
cutoff.setHours(10, 0, 0, 0);
// 1. Check if we already ordered for today
let hasOrder = false;
// Optimization: Check orderMap for today's date
// Keys are "YYYY-MM-DD_ArticleID"
for (const key of orderMap.keys()) {
if (key.startsWith(todayStr)) {
hasOrder = true;
break;
const diff = cutoff - now;
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
// User req: "heute noch keine bestellung... countdown erscheinen"
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
if (diff <= 0) {
removeCountdown();
return;
}
// 3. Render Countdown
const diffHrs = Math.floor(diff / 3600000);
const diffMins = Math.floor((diff % 3600000) / 60000);
const headerCenter = document.querySelector('.header-center-wrapper');
if (!headerCenter) return;
let countdownEl = document.getElementById('order-countdown');
if (!countdownEl) {
countdownEl = document.createElement('div');
countdownEl.id = 'order-countdown';
// Insert before cost display or append
headerCenter.insertBefore(countdownEl, headerCenter.firstChild);
}
countdownEl.innerHTML = `<span>Bestellschluss:</span> <strong>${diffHrs}h ${diffMins}m</strong>`;
// Red Alert if < 1 hour
if (diff < 3600000) { // 1 hour
countdownEl.classList.add('urgent');
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
icon: '⏳'
});
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
}
if (hasOrder) {
removeCountdown();
return;
}
// 2. Calculate time to cutoff (10:00 AM)
const cutoff = new Date();
cutoff.setHours(10, 0, 0, 0);
const diff = cutoff - now;
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
// User req: "heute noch keine bestellung... countdown erscheinen"
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
if (diff <= 0) {
removeCountdown();
return;
}
// 3. Render Countdown
const diffHrs = Math.floor(diff / 3600000);
const diffMins = Math.floor((diff % 3600000) / 60000);
const headerCenter = document.querySelector('.header-center-wrapper');
if (!headerCenter) return;
let countdownEl = document.getElementById('order-countdown');
if (!countdownEl) {
countdownEl = document.createElement('div');
countdownEl.id = 'order-countdown';
// Insert before cost display or append
headerCenter.insertBefore(countdownEl, headerCenter.firstChild);
}
countdownEl.innerHTML = `<span>Bestellschluss:</span> <strong>${diffHrs}h ${diffMins}m</strong>`;
// Red Alert if < 1 hour
if (diff < 3600000) { // 1 hour
countdownEl.classList.add('urgent');
// Notification logic (One time)
const notifiedKey = `kantine_notified_${todayStr}`;
if (!sessionStorage.getItem(notifiedKey)) {
if (Notification.permission === 'granted') {
new Notification('Kantine: Bestellschluss naht!', {
body: 'Du hast heute noch nichts bestellt. Nur noch 1 Stunde!',
icon: '⏳'
});
} else if (Notification.permission === 'default') {
Notification.requestPermission();
}
sessionStorage.setItem(notifiedKey, 'true');
}
} else {
countdownEl.classList.remove('urgent');
}
} else {
countdownEl.classList.remove('urgent');
}
}
function removeCountdown() {
const el = document.getElementById('order-countdown');
if (el) el.remove();
}
function removeCountdown() {
const el = document.getElementById('order-countdown');
if (el) el.remove();
}
// Update countdown every minute
setInterval(updateCountdown, 60000);
// Also update on load
setTimeout(updateCountdown, 1000);
// Update countdown every minute
setInterval(updateCountdown, 60000);
// Also update on load
setTimeout(updateCountdown, 1000);
// === Helpers ===
function getISOWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
}
// === Helpers ===
function getISOWeek(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const dayNum = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
}
function getWeekYear(d) {
const date = new Date(d.getTime());
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
return date.getFullYear();
}
function getWeekYear(d) {
const date = new Date(d.getTime());
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
return date.getFullYear();
}
function translateDay(englishDay) {
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
return map[englishDay] || englishDay;
}
function translateDay(englishDay) {
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
return map[englishDay] || englishDay;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text || '';
return div.innerHTML;
}
// === Bootstrap ===
injectUI();
bindEvents();
updateAuthUI();
cleanupExpiredFlags();
// === Bootstrap ===
injectUI();
bindEvents();
updateAuthUI();
cleanupExpiredFlags();
// Load cached data first for instant UI, then refresh from API
const hadCache = loadMenuCache();
if (hadCache) {
// Hide loading spinner since cache is shown
document.getElementById('loading').classList.add('hidden');
}
loadMenuDataFromAPI();
// Load cached data first for instant UI, then refresh from API
const hadCache = loadMenuCache();
if (hadCache) {
// Hide loading spinner since cache is shown
document.getElementById('loading').classList.add('hidden');
}
loadMenuDataFromAPI();
// Auto-start polling if already logged in
if (authToken) {
startPolling();
}
// Auto-start polling if already logged in
if (authToken) {
startPolling();
}
// Check for updates
checkForUpdates();
// Check for updates
checkForUpdates();
console.log('Kantine Wrapper loaded ✅');
})();
console.log('Kantine Wrapper loaded ✅');
}) ();
// === Error Modal ===
function showErrorModal(title, htmlContent, btnText, url) {