feat(ui): display matched highlight tags in menu cards (v1.2.4)

This commit is contained in:
2026-02-16 22:19:41 +01:00
parent f9b29254f9
commit 7296901ad9
8 changed files with 349 additions and 272 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

16
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1374,6 +1374,41 @@ body {
}
/* 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 {
font-size: 0.8rem;
background: rgba(0, 0, 0, 0.1);
@@ -1667,7 +1702,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.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>
</div>
@@ -2306,9 +2341,9 @@ body {
}
function checkHighlight(text) {
if (!text) return false;
if (!text) return [];
text = text.toLowerCase();
return highlightTags.some(tag => text.includes(tag));
return highlightTags.filter(tag => text.includes(tag));
}
// === Local Menu Cache (localStorage) ===
@@ -3005,7 +3040,7 @@ body {
// === Version Check ===
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 InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
@@ -3067,154 +3102,154 @@ body {
lastUpdatedIcon.replaceWith(updateLink);
}
}
} 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;
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
if (hasOrder) {
removeCountdown();
return;
}
// === 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;
}
// 2. Calculate time to cutoff (10:00 AM)
const cutoff = new Date();
cutoff.setHours(10, 0, 0, 0);
const todayStr = now.toISOString().split('T')[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();
// 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;
}
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() {
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) {