feat(ui): display matched highlight tags in menu cards (v1.2.4)
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
## v1.2.4 (2026-02-16)
|
||||||
|
- **Feature**: Gefundene Highlights werden jetzt direkt im Menü als Badge angezeigt. 🏷️
|
||||||
|
|
||||||
## v1.2.3 (2026-02-16)
|
## v1.2.3 (2026-02-16)
|
||||||
- **Fix**: Update-Icon ist jetzt klickbar und führt direkt zum Installer. 🔗
|
- **Fix**: Update-Icon ist jetzt klickbar und führt direkt zum Installer. 🔗
|
||||||
- **Dev**: Unit-Tests für Update-Logik im Build integriert. 🛡️
|
- **Dev**: Unit-Tests für Update-Logik im Build integriert. 🛡️
|
||||||
|
|||||||
4
dist/bookmarklet-payload.js
vendored
4
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
16
dist/install.html
vendored
16
dist/install.html
vendored
File diff suppressed because one or more lines are too long
299
dist/kantine-standalone.html
vendored
299
dist/kantine-standalone.html
vendored
@@ -1374,6 +1374,41 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Update Banner Enhanced */
|
/* Update Banner Enhanced */
|
||||||
|
.update-banner {
|
||||||
|
/* ... existing styles ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matched Tags in Menu Card */
|
||||||
|
.matched-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
/* Space between tags and title */
|
||||||
|
margin-top: -5px;
|
||||||
|
/* Pull closer to header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-badge-small {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #60a5fa;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .tag-badge-small {
|
||||||
|
background: rgba(37, 99, 235, 0.1);
|
||||||
|
color: #2563eb;
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.change-summary {
|
.change-summary {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
@@ -1667,7 +1702,7 @@ body {
|
|||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.3</small></h1>
|
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.4</small></h1>
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2306,9 +2341,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkHighlight(text) {
|
function checkHighlight(text) {
|
||||||
if (!text) return false;
|
if (!text) return [];
|
||||||
text = text.toLowerCase();
|
text = text.toLowerCase();
|
||||||
return highlightTags.some(tag => text.includes(tag));
|
return highlightTags.filter(tag => text.includes(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Local Menu Cache (localStorage) ===
|
// === Local Menu Cache (localStorage) ===
|
||||||
@@ -3005,7 +3040,7 @@ body {
|
|||||||
|
|
||||||
// === Version Check ===
|
// === Version Check ===
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
const CurrentVersion = 'v1.2.3';
|
const CurrentVersion = 'v1.2.4';
|
||||||
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
|
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';
|
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
|
||||||
|
|
||||||
@@ -3067,154 +3102,154 @@ body {
|
|||||||
lastUpdatedIcon.replaceWith(updateLink);
|
lastUpdatedIcon.replaceWith(updateLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Kantine] Version check failed:', 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOrder) {
|
// === Order Countdown ===
|
||||||
removeCountdown();
|
function updateCountdown() {
|
||||||
return;
|
const now = new Date();
|
||||||
}
|
const currentDay = now.getDay();
|
||||||
|
// Skip weekends (0=Sun, 6=Sat)
|
||||||
|
if (currentDay === 0 || currentDay === 6) {
|
||||||
|
removeCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Calculate time to cutoff (10:00 AM)
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
const cutoff = new Date();
|
|
||||||
cutoff.setHours(10, 0, 0, 0);
|
|
||||||
|
|
||||||
const diff = cutoff - now;
|
// 1. Check if we already ordered for today
|
||||||
|
let hasOrder = false;
|
||||||
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
|
// Optimization: Check orderMap for today's date
|
||||||
// User req: "heute noch keine bestellung... countdown erscheinen"
|
// Keys are "YYYY-MM-DD_ArticleID"
|
||||||
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
|
for (const key of orderMap.keys()) {
|
||||||
|
if (key.startsWith(todayStr)) {
|
||||||
if (diff <= 0) {
|
hasOrder = true;
|
||||||
removeCountdown();
|
break;
|
||||||
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');
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function removeCountdown() {
|
function removeCountdown() {
|
||||||
const el = document.getElementById('order-countdown');
|
const el = document.getElementById('order-countdown');
|
||||||
if (el) el.remove();
|
if (el) el.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update countdown every minute
|
// Update countdown every minute
|
||||||
setInterval(updateCountdown, 60000);
|
setInterval(updateCountdown, 60000);
|
||||||
// Also update on load
|
// Also update on load
|
||||||
setTimeout(updateCountdown, 1000);
|
setTimeout(updateCountdown, 1000);
|
||||||
|
|
||||||
// === Helpers ===
|
// === Helpers ===
|
||||||
function getISOWeek(date) {
|
function getISOWeek(date) {
|
||||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
const dayNum = d.getUTCDay() || 7;
|
const dayNum = d.getUTCDay() || 7;
|
||||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||||
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekYear(d) {
|
function getWeekYear(d) {
|
||||||
const date = new Date(d.getTime());
|
const date = new Date(d.getTime());
|
||||||
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
||||||
return date.getFullYear();
|
return date.getFullYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function translateDay(englishDay) {
|
function translateDay(englishDay) {
|
||||||
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
|
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
|
||||||
return map[englishDay] || englishDay;
|
return map[englishDay] || englishDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text || '';
|
div.textContent = text || '';
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Bootstrap ===
|
// === Bootstrap ===
|
||||||
injectUI();
|
injectUI();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
updateAuthUI();
|
updateAuthUI();
|
||||||
cleanupExpiredFlags();
|
cleanupExpiredFlags();
|
||||||
|
|
||||||
// Load cached data first for instant UI, then refresh from API
|
// Load cached data first for instant UI, then refresh from API
|
||||||
const hadCache = loadMenuCache();
|
const hadCache = loadMenuCache();
|
||||||
if (hadCache) {
|
if (hadCache) {
|
||||||
// Hide loading spinner since cache is shown
|
// Hide loading spinner since cache is shown
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
}
|
}
|
||||||
loadMenuDataFromAPI();
|
loadMenuDataFromAPI();
|
||||||
|
|
||||||
// Auto-start polling if already logged in
|
// Auto-start polling if already logged in
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for updates
|
// Check for updates
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
|
||||||
console.log('Kantine Wrapper loaded ✅');
|
console.log('Kantine Wrapper loaded ✅');
|
||||||
}) ();
|
})();
|
||||||
|
|
||||||
// === Error Modal ===
|
// === Error Modal ===
|
||||||
function showErrorModal(title, htmlContent, btnText, url) {
|
function showErrorModal(title, htmlContent, btnText, url) {
|
||||||
|
|||||||
260
kantine.js
260
kantine.js
@@ -705,9 +705,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkHighlight(text) {
|
function checkHighlight(text) {
|
||||||
if (!text) return false;
|
if (!text) return [];
|
||||||
text = text.toLowerCase();
|
text = text.toLowerCase();
|
||||||
return highlightTags.some(tag => text.includes(tag));
|
return highlightTags.filter(tag => text.includes(tag));
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Local Menu Cache (localStorage) ===
|
// === Local Menu Cache (localStorage) ===
|
||||||
@@ -1466,154 +1466,154 @@
|
|||||||
lastUpdatedIcon.replaceWith(updateLink);
|
lastUpdatedIcon.replaceWith(updateLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Kantine] Version check failed:', 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOrder) {
|
// === Order Countdown ===
|
||||||
removeCountdown();
|
function updateCountdown() {
|
||||||
return;
|
const now = new Date();
|
||||||
}
|
const currentDay = now.getDay();
|
||||||
|
// Skip weekends (0=Sun, 6=Sat)
|
||||||
|
if (currentDay === 0 || currentDay === 6) {
|
||||||
|
removeCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Calculate time to cutoff (10:00 AM)
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
const cutoff = new Date();
|
|
||||||
cutoff.setHours(10, 0, 0, 0);
|
|
||||||
|
|
||||||
const diff = cutoff - now;
|
// 1. Check if we already ordered for today
|
||||||
|
let hasOrder = false;
|
||||||
// If passed cutoff or more than 3 hours away (e.g. 07:00), maybe don't show?
|
// Optimization: Check orderMap for today's date
|
||||||
// User req: "heute noch keine bestellung... countdown erscheinen"
|
// Keys are "YYYY-MM-DD_ArticleID"
|
||||||
// Let's show it if within valid order window (e.g. 00:00 - 10:00)
|
for (const key of orderMap.keys()) {
|
||||||
|
if (key.startsWith(todayStr)) {
|
||||||
if (diff <= 0) {
|
hasOrder = true;
|
||||||
removeCountdown();
|
break;
|
||||||
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');
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function removeCountdown() {
|
function removeCountdown() {
|
||||||
const el = document.getElementById('order-countdown');
|
const el = document.getElementById('order-countdown');
|
||||||
if (el) el.remove();
|
if (el) el.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update countdown every minute
|
// Update countdown every minute
|
||||||
setInterval(updateCountdown, 60000);
|
setInterval(updateCountdown, 60000);
|
||||||
// Also update on load
|
// Also update on load
|
||||||
setTimeout(updateCountdown, 1000);
|
setTimeout(updateCountdown, 1000);
|
||||||
|
|
||||||
// === Helpers ===
|
// === Helpers ===
|
||||||
function getISOWeek(date) {
|
function getISOWeek(date) {
|
||||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
const dayNum = d.getUTCDay() || 7;
|
const dayNum = d.getUTCDay() || 7;
|
||||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||||
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWeekYear(d) {
|
function getWeekYear(d) {
|
||||||
const date = new Date(d.getTime());
|
const date = new Date(d.getTime());
|
||||||
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
|
||||||
return date.getFullYear();
|
return date.getFullYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function translateDay(englishDay) {
|
function translateDay(englishDay) {
|
||||||
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
|
const map = { Monday: 'Montag', Tuesday: 'Dienstag', Wednesday: 'Mittwoch', Thursday: 'Donnerstag', Friday: 'Freitag', Saturday: 'Samstag', Sunday: 'Sonntag' };
|
||||||
return map[englishDay] || englishDay;
|
return map[englishDay] || englishDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text || '';
|
div.textContent = text || '';
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Bootstrap ===
|
// === Bootstrap ===
|
||||||
injectUI();
|
injectUI();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
updateAuthUI();
|
updateAuthUI();
|
||||||
cleanupExpiredFlags();
|
cleanupExpiredFlags();
|
||||||
|
|
||||||
// Load cached data first for instant UI, then refresh from API
|
// Load cached data first for instant UI, then refresh from API
|
||||||
const hadCache = loadMenuCache();
|
const hadCache = loadMenuCache();
|
||||||
if (hadCache) {
|
if (hadCache) {
|
||||||
// Hide loading spinner since cache is shown
|
// Hide loading spinner since cache is shown
|
||||||
document.getElementById('loading').classList.add('hidden');
|
document.getElementById('loading').classList.add('hidden');
|
||||||
}
|
}
|
||||||
loadMenuDataFromAPI();
|
loadMenuDataFromAPI();
|
||||||
|
|
||||||
// Auto-start polling if already logged in
|
// Auto-start polling if already logged in
|
||||||
if (authToken) {
|
if (authToken) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for updates
|
// Check for updates
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
|
||||||
console.log('Kantine Wrapper loaded ✅');
|
console.log('Kantine Wrapper loaded ✅');
|
||||||
}) ();
|
})();
|
||||||
|
|
||||||
// === Error Modal ===
|
// === Error Modal ===
|
||||||
function showErrorModal(title, htmlContent, btnText, url) {
|
function showErrorModal(title, htmlContent, btnText, url) {
|
||||||
|
|||||||
35
style.css
35
style.css
@@ -1363,6 +1363,41 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Update Banner Enhanced */
|
/* Update Banner Enhanced */
|
||||||
|
.update-banner {
|
||||||
|
/* ... existing styles ... */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matched Tags in Menu Card */
|
||||||
|
.matched-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
/* Space between tags and title */
|
||||||
|
margin-top: -5px;
|
||||||
|
/* Pull closer to header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-badge-small {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #60a5fa;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .tag-badge-small {
|
||||||
|
background: rgba(37, 99, 235, 0.1);
|
||||||
|
color: #2563eb;
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.change-summary {
|
.change-summary {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.2.3
|
v1.2.4
|
||||||
|
|||||||
Reference in New Issue
Block a user