Compare commits

...

17 Commits

Author SHA1 Message Date
71cb2e8475 fix: use htmlpreview.github.io for update link (v1.0.3) 2026-02-13 11:49:51 +01:00
18c4961402 build: update dist files for v1.0.2 2026-02-13 11:47:15 +01:00
Michael
25816b7fb5 Bump version from v1.0.1 to v1.0.2 - update test 2026-02-13 11:43:09 +01:00
d002e6a900 feat: add auto-update check (v1.0.1) 2026-02-13 11:40:45 +01:00
c58b54faf6 chore: bump version to v1.0.0 for initial release 2026-02-13 11:20:13 +01:00
bff7ce3874 fix: resolve version number conflict with Bessa global variable (v1.8.6) 2026-02-13 11:16:31 +01:00
6864838999 feat: show version number in app header (v1.8.6) 2026-02-13 11:13:34 +01:00
6f2a34ae0e feat: inject version number into installer and bookmarklet (v1.8.5) 2026-02-13 11:11:46 +01:00
7008b40987 fix: correct day header status logic and text color (v1.8.4) 2026-02-13 11:06:22 +01:00
3500790b5d feat: color-code day headers based on status (v1.8.3) 2026-02-13 10:54:29 +01:00
3d185140cc docs: revert to dark theme with teal accents (v1.8.2) 2026-02-13 09:36:35 +01:00
35e59a15a0 docs: theme installer with Knapp colors (v1.8.1) 2026-02-13 09:33:28 +01:00
57d9e2e2b2 docs: update readme and installer content (v1.8.0) 2026-02-13 09:23:16 +01:00
93e110639c feat: refine next week badge violet logic (v1.7.4) 2026-02-13 08:38:55 +01:00
689d185635 feat: enhance next week badge and fix data persistence (v1.7.3) 2026-02-13 08:31:34 +01:00
8bddff10cd feat: add connectivity check and fallback modal (v1.7.2) 2026-02-12 13:15:43 +01:00
d5f1a51dc6 feat: add menu code badges to day header (v1.7.1) 2026-02-12 12:54:42 +01:00
10 changed files with 863 additions and 55 deletions

View File

@@ -8,9 +8,8 @@ Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses
* **Bestellstatus:** Farbige Indikatoren für bestellte Menüs. * **Bestellstatus:** Farbige Indikatoren für bestellte Menüs.
* **Kostenkontrolle:** Summiert automatisch den Gesamtpreis der Woche. * **Kostenkontrolle:** Summiert automatisch den Gesamtpreis der Woche.
* **Session Reuse:** Nutzt automatisch eine bestehende Login-Session (Loggt dich automatisch ein). * **Session Reuse:** Nutzt automatisch eine bestehende Login-Session (Loggt dich automatisch ein).
* **Bestellhistorie:** Zeigt zuverlässig alle aktiven und abgeschlossenen Bestellungen an (über `/user/orders/`). * **API Fallback:** Prüft die Verbindung und bietet bei Fehlern einen Direktlink zur Originalseite.
* **Lokaler Cache:** Lädt Menüdaten blitzschnell aus dem Browser-Speicher. * **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header.
* **Scroll-Fix:** Garantiert Scrollbarkeit auch auf restriktiven Seiten.
## 📦 Installation ## 📦 Installation

View File

@@ -8,19 +8,28 @@ DIST_DIR="$SCRIPT_DIR/dist"
CSS_FILE="$SCRIPT_DIR/style.css" CSS_FILE="$SCRIPT_DIR/style.css"
JS_FILE="$SCRIPT_DIR/kantine.js" JS_FILE="$SCRIPT_DIR/kantine.js"
# === VERSION ===
if [ -f "$SCRIPT_DIR/version.txt" ]; then
VERSION=$(cat "$SCRIPT_DIR/version.txt" | tr -d '\n')
else
echo "ERROR: version.txt not found"
exit 1
fi
mkdir -p "$DIST_DIR" mkdir -p "$DIST_DIR"
echo "=== Kantine Bookmarklet Builder ===" echo "=== Kantine Bookmarklet Builder ($VERSION) ==="
# Check files exist # Check files exist
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
CSS_CONTENT=$(cat "$CSS_FILE") CSS_CONTENT=$(cat "$CSS_FILE")
JS_CONTENT=$(cat "$JS_FILE") # Inject version into JS
JS_CONTENT=$(cat "$JS_FILE" | sed "s/{{VERSION}}/$VERSION/g")
# === 1. Build standalone HTML (for local testing/dev) === # === 1. Build standalone HTML (for local testing/dev) ===
cat > "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF' cat > "$DIST_DIR/kantine-standalone.html" << HTMLEOF
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
@@ -37,7 +46,7 @@ HTMLEOF
# Inject CSS # Inject CSS
cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html" cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF' cat >> "$DIST_DIR/kantine-standalone.html" << HTMLEOF
</style> </style>
</head> </head>
<body> <body>
@@ -45,9 +54,9 @@ cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
HTMLEOF HTMLEOF
# Inject JS # Inject JS
cat "$JS_FILE" >> "$DIST_DIR/kantine-standalone.html" echo "$JS_CONTENT" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF' cat >> "$DIST_DIR/kantine-standalone.html" << HTMLEOF
</script> </script>
</body> </body>
</html> </html>
@@ -69,7 +78,7 @@ var s=document.createElement('style');
s.textContent='${CSS_ESCAPED}'; s.textContent='${CSS_ESCAPED}';
document.head.appendChild(s); document.head.appendChild(s);
var sc=document.createElement('script'); var sc=document.createElement('script');
sc.textContent=$(cat "$JS_FILE" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || cat "$JS_FILE" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/'); sc.textContent=$(echo "$JS_CONTENT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "$JS_CONTENT" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/');
document.head.appendChild(sc); document.head.appendChild(sc);
})(); })();
PAYLOADEOF PAYLOADEOF
@@ -81,31 +90,45 @@ echo "javascript:${BOOKMARKLET_RAW}" > "$DIST_DIR/bookmarklet.txt"
echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt" echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt"
# === 3. Create an easy-to-use HTML installer page === # === 3. Create an easy-to-use HTML installer page ===
cat > "$DIST_DIR/install.html" << 'INSTALLEOF' cat > "$DIST_DIR/install.html" << INSTALLEOF
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Kantine Wrapper Installer</title> <title>Kantine Wrapper Installer ($VERSION)</title>
<style> <style>
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; } h1 { color: #029AA8; } /* Knapp Teal */
.instructions { background: #16213e; padding: 20px; border-radius: 12px; margin: 20px 0; } .instructions { background: #16213e; padding: 20px; border-radius: 12px; margin: 20px 0; }
.instructions ol li { margin: 10px 0; } .instructions ol li { margin: 10px 0; }
a.bookmarklet { display: inline-block; background: #e94560; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 18px; cursor: grab; } a.bookmarklet { display: inline-block; background: #029AA8; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 18px; cursor: grab; }
a.bookmarklet:hover { background: #c73652; } a.bookmarklet:hover { background: #006269; }
code { background: #0f3460; padding: 2px 6px; border-radius: 4px; } code { background: #0f3460; padding: 2px 6px; border-radius: 4px; }
</style> </style>
</head> </head>
<body> <body>
<h1>🍽️ Kantine Wrapper</h1> <h1>🍽️ Kantine Wrapper <span style="font-size:0.5em; opacity:0.6; font-weight:400; vertical-align:middle; margin-left:10px;">$VERSION</span></h1>
<div class="instructions"> <div class="instructions">
<h2>Installation</h2> <h2>Installation</h2>
<ol> <ol>
<li>Ziehe den Button unten in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li> <li>Ziehe den Button unten in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li>
<li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#e94560">web.bessa.app/knapp-kantine</a></li> <li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#029AA8">web.bessa.app/knapp-kantine</a></li>
<li>Klicke auf das Lesezeichen <code>Kantine Wrapper</code></li> <li>Klicke auf das Lesezeichen <code>Kantine $VERSION</code></li>
</ol> </ol>
<h2>✨ Features</h2>
<ul>
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li>
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li>
<li>🏷️ <strong>Badges & Status:</strong> Menü-Codes (M1, M2) und Bestellstatus direkt sichtbar.</li>
<li>🛡️ <strong>Offline-Support:</strong> Speichert Menüdaten lokal.</li>
</ul>
<div style="margin-top: 30px; padding: 15px; background: rgba(233, 69, 96, 0.1); border: 1px solid rgba(233, 69, 96, 0.3); border-radius: 8px; font-size: 0.85em; color: #ddd;">
<strong>⚠️ Haftungsausschluss:</strong><br>
Die Verwendung dieses Bookmarklets erfolgt auf eigene Verantwortung. Der Entwickler übernimmt keine Haftung für Schäden, Datenverlust oder ungewollte Bestellungen, die durch die Nutzung dieser Software entstehen.
</div>
</div> </div>
<p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p> <p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p> <p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
@@ -114,7 +137,7 @@ INSTALLEOF
# Embed the bookmarklet URL inline # Embed the bookmarklet URL inline
echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html" echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html"
cat "$JS_FILE" | python3 -c " echo "$JS_CONTENT" | python3 -c "
import sys, json import sys, json
js = sys.stdin.read() js = sys.stdin.read()
css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ') css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ')
@@ -122,8 +145,8 @@ bmk = '''javascript:(function(){if(window.__KANTINE_LOADED){alert(\"Already load
print(json.dumps(bmk) + ';') print(json.dumps(bmk) + ';')
" 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html" " 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html"
cat >> "$DIST_DIR/install.html" << 'INSTALLEOF' cat >> "$DIST_DIR/install.html" << INSTALLEOF
document.getElementById('bookmarklet-link').textContent = '🍽️ Kantine Wrapper'; document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
</script> </script>
</body> </body>
</html> </html>

150
build-bookmarklet_old.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# Build script for Kantine Bookmarklet
# Creates a self-contained bookmarklet URL and standalone HTML file
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DIST_DIR="$SCRIPT_DIR/dist"
CSS_FILE="$SCRIPT_DIR/style.css"
JS_FILE="$SCRIPT_DIR/kantine.js"
mkdir -p "$DIST_DIR"
echo "=== Kantine Bookmarklet Builder ==="
# Check files exist
if [ ! -f "$CSS_FILE" ]; then echo "ERROR: $CSS_FILE not found"; exit 1; fi
if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
CSS_CONTENT=$(cat "$CSS_FILE")
JS_CONTENT=$(cat "$JS_FILE")
# === 1. Build standalone HTML (for local testing/dev) ===
cat > "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kantine Weekly Menu (Standalone)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
<style>
HTMLEOF
# Inject CSS
cat "$CSS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
</style>
</head>
<body>
<script>
HTMLEOF
# Inject JS
cat "$JS_FILE" >> "$DIST_DIR/kantine-standalone.html"
cat >> "$DIST_DIR/kantine-standalone.html" << 'HTMLEOF'
</script>
</body>
</html>
HTMLEOF
echo "✅ Standalone HTML: $DIST_DIR/kantine-standalone.html"
# === 2. Build bookmarklet (JavaScript URL) ===
# The bookmarklet injects CSS + JS into the current page
# Escape CSS for embedding in JS string
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
# Build bookmarklet payload
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
(function(){
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
var s=document.createElement('style');
s.textContent='${CSS_ESCAPED}';
document.head.appendChild(s);
var sc=document.createElement('script');
sc.textContent=$(cat "$JS_FILE" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || cat "$JS_FILE" | sed 's/\\/\\\\/g' | sed "s/'/\\\\'/g" | sed 's/"/\\\\"/g' | tr '\n' ' ' | sed 's/^/"/' | sed 's/$/"/');
document.head.appendChild(sc);
})();
PAYLOADEOF
# URL-encode for bookmark
BOOKMARKLET_RAW=$(cat "$DIST_DIR/bookmarklet-payload.js" | tr '\n' ' ' | sed 's/ */ /g')
echo "javascript:${BOOKMARKLET_RAW}" > "$DIST_DIR/bookmarklet.txt"
echo "✅ Bookmarklet URL: $DIST_DIR/bookmarklet.txt"
# === 3. Create an easy-to-use HTML installer page ===
cat > "$DIST_DIR/install.html" << 'INSTALLEOF'
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Kantine Wrapper Installer</title>
<style>
body { font-family: 'Inter', sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #e94560; }
.instructions { background: #16213e; padding: 20px; border-radius: 12px; margin: 20px 0; }
.instructions ol li { margin: 10px 0; }
a.bookmarklet { display: inline-block; background: #e94560; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 18px; cursor: grab; }
a.bookmarklet:hover { background: #c73652; }
code { background: #0f3460; padding: 2px 6px; border-radius: 4px; }
</style>
</head>
<body>
<h1>🍽️ Kantine Wrapper</h1>
<div class="instructions">
<h2>Installation</h2>
<ol>
<li>Ziehe den Button unten in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</li>
<li>Navigiere zu <a href="https://web.bessa.app/knapp-kantine" style="color:#e94560">web.bessa.app/knapp-kantine</a></li>
<li>Klicke auf das Lesezeichen <code>Kantine Wrapper</code></li>
</ol>
<h2>✨ Features</h2>
<ul>
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li>
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li>
<li>🏷️ <strong>Badges & Status:</strong> Menü-Codes (M1, M2) und Bestellstatus direkt sichtbar.</li>
<li>🛡️ <strong>Offline-Support:</strong> Speichert Menüdaten lokal.</li>
</ul>
<div style="margin-top: 30px; padding: 15px; background: rgba(233, 69, 96, 0.1); border: 1px solid rgba(233, 69, 96, 0.3); border-radius: 8px; font-size: 0.85em; color: #ddd;">
<strong>⚠️ Haftungsausschluss:</strong><br>
Die Verwendung dieses Bookmarklets erfolgt auf eigene Verantwortung. Der Entwickler übernimmt keine Haftung für Schäden, Datenverlust oder ungewollte Bestellungen, die durch die Nutzung dieser Software entstehen.
</div>
</div>
<p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
<script>
INSTALLEOF
# Embed the bookmarklet URL inline
echo "document.getElementById('bookmarklet-link').href = " >> "$DIST_DIR/install.html"
cat "$JS_FILE" | python3 -c "
import sys, json
js = sys.stdin.read()
css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ')
bmk = '''javascript:(function(){if(window.__KANTINE_LOADED){alert(\"Already loaded\");return;}var s=document.createElement(\"style\");s.textContent=''' + json.dumps(css) + ''';document.head.appendChild(s);var sc=document.createElement(\"script\");sc.textContent=''' + json.dumps(js) + ''';document.head.appendChild(sc);})();'''
print(json.dumps(bmk) + ';')
" 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html"
cat >> "$DIST_DIR/install.html" << 'INSTALLEOF'
document.getElementById('bookmarklet-link').textContent = '🍽️ Kantine Wrapper';
</script>
</body>
</html>
INSTALLEOF
echo "✅ Installer page: $DIST_DIR/install.html"
echo ""
echo "=== Build Complete ==="
echo "Files in $DIST_DIR:"
ls -la "$DIST_DIR/"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

32
dist/install.html vendored

File diff suppressed because one or more lines are too long

View File

@@ -1115,7 +1115,98 @@ body {
100% { 100% {
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
} }
} </style> }
/* Day Header Badges */
.day-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.menu-code-badge {
font-size: 0.75rem;
font-weight: 700;
color: #8b5cf6;
/* Violet 500 */
background-color: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3);
padding: 2px 6px;
border-radius: 6px;
line-height: normal;
display: inline-block;
}
/* Detailed Badge Colors */
.nav-badge.badge-violet {
background-color: #8b5cf6;
}
.nav-badge.badge-green {
background-color: var(--success-color);
}
.nav-badge.badge-red {
background-color: var(--error-color);
}
.nav-badge.badge-blue {
background-color: var(--accent-color);
}
/* Day Header Status Colors (User Request) */
.card-header.header-violet {
background-color: rgba(139, 92, 246, 0.15);
border-bottom: 2px solid #8b5cf6;
}
.card-header.header-green {
background-color: rgba(16, 185, 129, 0.15);
border-bottom: 2px solid var(--success-color);
}
.card-header.header-red {
background-color: rgba(239, 68, 68, 0.15);
border-bottom: 2px solid var(--error-color);
}
.card-header.header-violet .day-name,
.card-header.header-green .day-name,
.card-header.header-red .day-name {
font-weight: 700;
color: var(--text-primary);
/* Ensure text remains standard color */
}
/* Update Icon */
.update-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
background-color: rgba(16, 185, 129, 0.2); /* Green tint */
color: var(--success-color);
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
animation: pulse 2s infinite;
}
.update-icon:hover {
background-color: var(--success-color);
color: white;
transform: scale(1.1);
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
</style>
</head> </head>
<body> <body>
<script> <script>
@@ -1186,8 +1277,8 @@ body {
<div class="header-content"> <div class="header-content">
<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="brand-text"> <div class="header-left">
<h1>Kantinen Übersicht</h1> <h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.0.3</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
</div> </div>
@@ -1851,8 +1942,23 @@ body {
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
} }
// 3. Group by ISO week // 3. Group by ISO week (Merge with existing to preserve past days)
const weeksMap = new Map(); const weeksMap = new Map();
// Hydrate from existing cache (preserve past data)
if (allWeeks && allWeeks.length > 0) {
allWeeks.forEach(w => {
const key = `${w.year}-${w.weekNumber}`;
try {
weeksMap.set(key, {
year: w.year,
weekNumber: w.weekNumber,
days: w.days ? w.days.map(d => ({ ...d, items: d.items ? [...d.items] : [] })) : []
});
} catch (e) { console.warn('Error hydrating week:', e); }
});
}
for (const day of allDays) { for (const day of allDays) {
const d = new Date(day.date); const d = new Date(day.date);
const weekNum = getISOWeek(d); const weekNum = getISOWeek(d);
@@ -1863,11 +1969,12 @@ body {
weeksMap.set(key, { year, weekNumber: weekNum, days: [] }); weeksMap.set(key, { year, weekNumber: weekNum, days: [] });
} }
const weekObj = weeksMap.get(key);
const weekday = d.toLocaleDateString('en-US', { weekday: 'long' }); const weekday = d.toLocaleDateString('en-US', { weekday: 'long' });
const orderCutoffDate = new Date(day.date); const orderCutoffDate = new Date(day.date);
orderCutoffDate.setHours(10, 0, 0, 0); orderCutoffDate.setHours(10, 0, 0, 0);
weeksMap.get(key).days.push({ const newDayObj = {
date: day.date, date: day.date,
weekday: weekday, weekday: weekday,
orderCutoff: orderCutoffDate.toISOString(), orderCutoff: orderCutoffDate.toISOString(),
@@ -1885,13 +1992,25 @@ body {
amountTracking: item.amount_tracking !== false amountTracking: item.amount_tracking !== false
}; };
}) })
}); };
// Merge: Overwrite if exists, push if new
const existingIndex = weekObj.days.findIndex(existing => existing.date === day.date);
if (existingIndex >= 0) {
weekObj.days[existingIndex] = newDayObj;
} else {
weekObj.days.push(newDayObj);
}
} }
// Sort weeks and days
allWeeks = Array.from(weeksMap.values()).sort((a, b) => { allWeeks = Array.from(weeksMap.values()).sort((a, b) => {
if (a.year !== b.year) return a.year - b.year; if (a.year !== b.year) return a.year - b.year;
return a.weekNumber - b.weekNumber; return a.weekNumber - b.weekNumber;
}); });
allWeeks.forEach(w => {
if (w.days) w.days.sort((a, b) => a.date.localeCompare(b.date));
});
// Save to localStorage cache // Save to localStorage cache
saveMenuCache(); saveMenuCache();
@@ -1913,8 +2032,14 @@ body {
} catch (error) { } catch (error) {
console.error('Error fetching menu:', error); console.error('Error fetching menu:', error);
document.getElementById('menu-container').innerHTML = `<p class="error">Fehler beim Laden der Menüdaten: ${error.message}</p>`;
progressModal.classList.add('hidden'); progressModal.classList.add('hidden');
showErrorModal(
'Keine Verbindung',
`Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.<br><br><small style="color:var(--text-secondary)">${error.message}</small>`,
'Zur Original-Seite',
'https://web.bessa.app/knapp-kantine'
);
} finally { } finally {
loading.classList.add('hidden'); loading.classList.add('hidden');
} }
@@ -1964,12 +2089,25 @@ body {
const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear); const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear);
let totalDataCount = 0; let totalDataCount = 0;
let orderableCount = 0; let orderableCount = 0;
let daysWithOrders = 0;
let daysWithOrderableAndNoOrder = 0;
if (nextWeekData && nextWeekData.days) { if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => { nextWeekData.days.forEach(day => {
if (day.items && day.items.length > 0) { if (day.items && day.items.length > 0) {
totalDataCount++; totalDataCount++;
if (day.items.some(item => item.available)) orderableCount++; const isOrderable = day.items.some(item => item.available);
if (isOrderable) orderableCount++;
let hasOrder = false;
day.items.forEach(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
if (orderMap.has(key) && orderMap.get(key).length > 0) hasOrder = true;
});
if (hasOrder) daysWithOrders++;
if (isOrderable && !hasOrder) daysWithOrderableAndNoOrder++;
} }
}); });
} }
@@ -1981,8 +2119,27 @@ body {
badge.className = 'nav-badge'; badge.className = 'nav-badge';
btnNextWeek.appendChild(badge); btnNextWeek.appendChild(badge);
} }
badge.title = `${orderableCount} Tage bestellbar / ${totalDataCount} Tage mit Menüdaten`;
badge.innerHTML = `<span class="orderable">${orderableCount}</span><span class="separator">/</span><span class="total">${totalDataCount}</span>`; // Format: ( Ordered / Orderable / Total )
badge.title = `${daysWithOrders} bestellt / ${orderableCount} bestellbar / ${totalDataCount} gesamt`;
badge.innerHTML = `<span class="ordered">${daysWithOrders}</span><span class="separator">/</span><span class="orderable">${orderableCount}</span><span class="separator">/</span><span class="total">${totalDataCount}</span>`;
// Color Logic
badge.classList.remove('badge-violet', 'badge-green', 'badge-red', 'badge-blue');
// Refined Logic (v1.7.4):
// Violet: If we have orders AND there are no DAYS left that are orderable but un-ordered.
// (i.e. "I have ordered everything I can")
if (daysWithOrders > 0 && daysWithOrderableAndNoOrder === 0) {
badge.classList.add('badge-violet');
} else if (daysWithOrderableAndNoOrder > 0) {
badge.classList.add('badge-green'); // Orderable days exist without order
} else if (orderableCount === 0) {
badge.classList.add('badge-red'); // No orderable days at all & no orders
} else {
badge.classList.add('badge-blue'); // Default / partial state
}
} else if (badge) { } else if (badge) {
badge.remove(); badge.remove();
} }
@@ -2120,12 +2277,66 @@ body {
if (isPastCutoff) card.classList.add('past-day'); if (isPastCutoff) card.classList.add('past-day');
// Collect ordered menu codes
const menuBadges = [];
if (day.items) {
day.items.forEach(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const orderKey = `${day.date}_${articleId}`;
const orders = orderMap.get(orderKey) || [];
const count = orders.length;
if (count > 0) {
// Regex for M1, M2, M1F etc.
const match = item.name.match(/([M][1-9][Ff]?)/);
if (match) {
let code = match[1];
if (count > 1) code += '+';
menuBadges.push(code);
}
}
});
}
// Header // Header
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'card-header'; header.className = 'card-header';
const dateStr = cardDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const dateStr = cardDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join('');
// Determine Day Status for Header Color
// Violet: Has Order
// Green: No Order but Orderable
// Red: No Order and Not Orderable (Locked/Sold Out)
let headerClass = '';
const hasAnyOrder = day.items && day.items.some(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
return orderMap.has(key) && orderMap.get(key).length > 0;
});
const hasOrderable = day.items && day.items.some(item => {
// Use pre-calculated available flag from loadMenuDataFromAPI calculation
return item.available;
});
if (hasAnyOrder) {
headerClass = 'header-violet';
} else if (hasOrderable && !isPastCutoff) {
headerClass = 'header-green';
} else {
// Red if not orderable (or past cutoff)
headerClass = 'header-red';
}
if (headerClass) header.classList.add(headerClass);
header.innerHTML = ` header.innerHTML = `
<div class="day-header-left">
<span class="day-name">${translateDay(day.weekday)}</span> <span class="day-name">${translateDay(day.weekday)}</span>
<div class="day-badges">${badgesHtml}</div>
</div>
<span class="day-date">${dateStr}</span>`; <span class="day-date">${dateStr}</span>`;
card.appendChild(header); card.appendChild(header);
@@ -2273,6 +2484,50 @@ body {
return card; return card;
} }
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = 'v1.0.3'; // Injected by build script
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
// Use htmlpreview.github.io to render the HTML directly in browser
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
try {
const response = await fetch(VersionUrl, { cache: 'no-cache' });
if (!response.ok) return;
const remoteVersion = (await response.text()).trim();
console.log(`[Kantine] Remote version: ${remoteVersion}`);
if (remoteVersion && remoteVersion !== CurrentVersion) {
// Simple semantic version check or string inequality
// Assuming format v1.0.0
showUpdateIcon(remoteVersion, InstallerUrl);
}
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
function showUpdateIcon(newVersion, url) {
const headerTitle = document.querySelector('.header-left h1');
if (!headerTitle) return;
// Check if already added
if (headerTitle.querySelector('.update-icon')) return;
const icon = document.createElement('a');
icon.className = 'update-icon';
icon.href = url;
icon.target = '_blank';
icon.innerHTML = '🆕'; // User requested icon
icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`;
headerTitle.appendChild(icon);
showToast(`Update verfügbar: ${newVersion}`, 'info');
}
// === 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()));
@@ -2318,8 +2573,64 @@ body {
startPolling(); startPolling();
} }
// Check for updates
checkForUpdates();
console.log('Kantine Wrapper loaded ✅'); console.log('Kantine Wrapper loaded ✅');
})(); })();
// === Error Modal ===
function showErrorModal(title, htmlContent, btnText, url) {
const modalId = 'error-modal';
let modal = document.getElementById(modalId);
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = modalId;
modal.className = 'modal hidden';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
<span class="material-icons-round">signal_wifi_off</span>
${title}
</h2>
</div>
<div style="padding: 20px;">
<p style="margin-bottom: 15px; color: var(--text-primary);">${htmlContent}</p>
<div style="margin-top: 20px; display: flex; justify-content: center;">
<button id="btn-error-redirect" style="
background-color: var(--accent-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
transition: transform 0.1s;
">
${btnText}
<span class="material-icons-round">open_in_new</span>
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('btn-error-redirect').addEventListener('click', () => {
window.location.href = url;
});
requestAnimationFrame(() => {
modal.classList.remove('hidden');
});
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -65,8 +65,8 @@
<div class="header-content"> <div class="header-content">
<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="brand-text"> <div class="header-left">
<h1>Kantinen Übersicht</h1> <h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">{{VERSION}}</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div> <div id="last-updated-subtitle" class="subtitle"></div>
</div> </div>
</div> </div>
@@ -730,8 +730,23 @@
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
} }
// 3. Group by ISO week // 3. Group by ISO week (Merge with existing to preserve past days)
const weeksMap = new Map(); const weeksMap = new Map();
// Hydrate from existing cache (preserve past data)
if (allWeeks && allWeeks.length > 0) {
allWeeks.forEach(w => {
const key = `${w.year}-${w.weekNumber}`;
try {
weeksMap.set(key, {
year: w.year,
weekNumber: w.weekNumber,
days: w.days ? w.days.map(d => ({ ...d, items: d.items ? [...d.items] : [] })) : []
});
} catch (e) { console.warn('Error hydrating week:', e); }
});
}
for (const day of allDays) { for (const day of allDays) {
const d = new Date(day.date); const d = new Date(day.date);
const weekNum = getISOWeek(d); const weekNum = getISOWeek(d);
@@ -742,11 +757,12 @@
weeksMap.set(key, { year, weekNumber: weekNum, days: [] }); weeksMap.set(key, { year, weekNumber: weekNum, days: [] });
} }
const weekObj = weeksMap.get(key);
const weekday = d.toLocaleDateString('en-US', { weekday: 'long' }); const weekday = d.toLocaleDateString('en-US', { weekday: 'long' });
const orderCutoffDate = new Date(day.date); const orderCutoffDate = new Date(day.date);
orderCutoffDate.setHours(10, 0, 0, 0); orderCutoffDate.setHours(10, 0, 0, 0);
weeksMap.get(key).days.push({ const newDayObj = {
date: day.date, date: day.date,
weekday: weekday, weekday: weekday,
orderCutoff: orderCutoffDate.toISOString(), orderCutoff: orderCutoffDate.toISOString(),
@@ -764,13 +780,25 @@
amountTracking: item.amount_tracking !== false amountTracking: item.amount_tracking !== false
}; };
}) })
}); };
// Merge: Overwrite if exists, push if new
const existingIndex = weekObj.days.findIndex(existing => existing.date === day.date);
if (existingIndex >= 0) {
weekObj.days[existingIndex] = newDayObj;
} else {
weekObj.days.push(newDayObj);
}
} }
// Sort weeks and days
allWeeks = Array.from(weeksMap.values()).sort((a, b) => { allWeeks = Array.from(weeksMap.values()).sort((a, b) => {
if (a.year !== b.year) return a.year - b.year; if (a.year !== b.year) return a.year - b.year;
return a.weekNumber - b.weekNumber; return a.weekNumber - b.weekNumber;
}); });
allWeeks.forEach(w => {
if (w.days) w.days.sort((a, b) => a.date.localeCompare(b.date));
});
// Save to localStorage cache // Save to localStorage cache
saveMenuCache(); saveMenuCache();
@@ -792,8 +820,14 @@
} catch (error) { } catch (error) {
console.error('Error fetching menu:', error); console.error('Error fetching menu:', error);
document.getElementById('menu-container').innerHTML = `<p class="error">Fehler beim Laden der Menüdaten: ${error.message}</p>`;
progressModal.classList.add('hidden'); progressModal.classList.add('hidden');
showErrorModal(
'Keine Verbindung',
`Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.<br><br><small style="color:var(--text-secondary)">${error.message}</small>`,
'Zur Original-Seite',
'https://web.bessa.app/knapp-kantine'
);
} finally { } finally {
loading.classList.add('hidden'); loading.classList.add('hidden');
} }
@@ -843,12 +877,25 @@
const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear); const nextWeekData = allWeeks.find(w => w.weekNumber === nextWeek && w.year === nextYear);
let totalDataCount = 0; let totalDataCount = 0;
let orderableCount = 0; let orderableCount = 0;
let daysWithOrders = 0;
let daysWithOrderableAndNoOrder = 0;
if (nextWeekData && nextWeekData.days) { if (nextWeekData && nextWeekData.days) {
nextWeekData.days.forEach(day => { nextWeekData.days.forEach(day => {
if (day.items && day.items.length > 0) { if (day.items && day.items.length > 0) {
totalDataCount++; totalDataCount++;
if (day.items.some(item => item.available)) orderableCount++; const isOrderable = day.items.some(item => item.available);
if (isOrderable) orderableCount++;
let hasOrder = false;
day.items.forEach(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
if (orderMap.has(key) && orderMap.get(key).length > 0) hasOrder = true;
});
if (hasOrder) daysWithOrders++;
if (isOrderable && !hasOrder) daysWithOrderableAndNoOrder++;
} }
}); });
} }
@@ -860,8 +907,27 @@
badge.className = 'nav-badge'; badge.className = 'nav-badge';
btnNextWeek.appendChild(badge); btnNextWeek.appendChild(badge);
} }
badge.title = `${orderableCount} Tage bestellbar / ${totalDataCount} Tage mit Menüdaten`;
badge.innerHTML = `<span class="orderable">${orderableCount}</span><span class="separator">/</span><span class="total">${totalDataCount}</span>`; // Format: ( Ordered / Orderable / Total )
badge.title = `${daysWithOrders} bestellt / ${orderableCount} bestellbar / ${totalDataCount} gesamt`;
badge.innerHTML = `<span class="ordered">${daysWithOrders}</span><span class="separator">/</span><span class="orderable">${orderableCount}</span><span class="separator">/</span><span class="total">${totalDataCount}</span>`;
// Color Logic
badge.classList.remove('badge-violet', 'badge-green', 'badge-red', 'badge-blue');
// Refined Logic (v1.7.4):
// Violet: If we have orders AND there are no DAYS left that are orderable but un-ordered.
// (i.e. "I have ordered everything I can")
if (daysWithOrders > 0 && daysWithOrderableAndNoOrder === 0) {
badge.classList.add('badge-violet');
} else if (daysWithOrderableAndNoOrder > 0) {
badge.classList.add('badge-green'); // Orderable days exist without order
} else if (orderableCount === 0) {
badge.classList.add('badge-red'); // No orderable days at all & no orders
} else {
badge.classList.add('badge-blue'); // Default / partial state
}
} else if (badge) { } else if (badge) {
badge.remove(); badge.remove();
} }
@@ -999,12 +1065,66 @@
if (isPastCutoff) card.classList.add('past-day'); if (isPastCutoff) card.classList.add('past-day');
// Collect ordered menu codes
const menuBadges = [];
if (day.items) {
day.items.forEach(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const orderKey = `${day.date}_${articleId}`;
const orders = orderMap.get(orderKey) || [];
const count = orders.length;
if (count > 0) {
// Regex for M1, M2, M1F etc.
const match = item.name.match(/([M][1-9][Ff]?)/);
if (match) {
let code = match[1];
if (count > 1) code += '+';
menuBadges.push(code);
}
}
});
}
// Header // Header
const header = document.createElement('div'); const header = document.createElement('div');
header.className = 'card-header'; header.className = 'card-header';
const dateStr = cardDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); const dateStr = cardDate.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
const badgesHtml = menuBadges.map(code => `<span class="menu-code-badge">${code}</span>`).join('');
// Determine Day Status for Header Color
// Violet: Has Order
// Green: No Order but Orderable
// Red: No Order and Not Orderable (Locked/Sold Out)
let headerClass = '';
const hasAnyOrder = day.items && day.items.some(item => {
const articleId = item.articleId || parseInt(item.id.split('_')[1]);
const key = `${day.date}_${articleId}`;
return orderMap.has(key) && orderMap.get(key).length > 0;
});
const hasOrderable = day.items && day.items.some(item => {
// Use pre-calculated available flag from loadMenuDataFromAPI calculation
return item.available;
});
if (hasAnyOrder) {
headerClass = 'header-violet';
} else if (hasOrderable && !isPastCutoff) {
headerClass = 'header-green';
} else {
// Red if not orderable (or past cutoff)
headerClass = 'header-red';
}
if (headerClass) header.classList.add(headerClass);
header.innerHTML = ` header.innerHTML = `
<div class="day-header-left">
<span class="day-name">${translateDay(day.weekday)}</span> <span class="day-name">${translateDay(day.weekday)}</span>
<div class="day-badges">${badgesHtml}</div>
</div>
<span class="day-date">${dateStr}</span>`; <span class="day-date">${dateStr}</span>`;
card.appendChild(header); card.appendChild(header);
@@ -1152,6 +1272,50 @@
return card; return card;
} }
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = '{{VERSION}}'; // Injected by build script
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
// Use htmlpreview.github.io to render the HTML directly in browser
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
try {
const response = await fetch(VersionUrl, { cache: 'no-cache' });
if (!response.ok) return;
const remoteVersion = (await response.text()).trim();
console.log(`[Kantine] Remote version: ${remoteVersion}`);
if (remoteVersion && remoteVersion !== CurrentVersion) {
// Simple semantic version check or string inequality
// Assuming format v1.0.0
showUpdateIcon(remoteVersion, InstallerUrl);
}
} catch (error) {
console.warn('[Kantine] Version check failed:', error);
}
}
function showUpdateIcon(newVersion, url) {
const headerTitle = document.querySelector('.header-left h1');
if (!headerTitle) return;
// Check if already added
if (headerTitle.querySelector('.update-icon')) return;
const icon = document.createElement('a');
icon.className = 'update-icon';
icon.href = url;
icon.target = '_blank';
icon.innerHTML = '🆕'; // User requested icon
icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`;
headerTitle.appendChild(icon);
showToast(`Update verfügbar: ${newVersion}`, 'info');
}
// === 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()));
@@ -1197,5 +1361,61 @@
startPolling(); startPolling();
} }
// Check for updates
checkForUpdates();
console.log('Kantine Wrapper loaded ✅'); console.log('Kantine Wrapper loaded ✅');
})(); })();
// === Error Modal ===
function showErrorModal(title, htmlContent, btnText, url) {
const modalId = 'error-modal';
let modal = document.getElementById(modalId);
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = modalId;
modal.className = 'modal hidden';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
<span class="material-icons-round">signal_wifi_off</span>
${title}
</h2>
</div>
<div style="padding: 20px;">
<p style="margin-bottom: 15px; color: var(--text-primary);">${htmlContent}</p>
<div style="margin-top: 20px; display: flex; justify-content: center;">
<button id="btn-error-redirect" style="
background-color: var(--accent-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
transition: transform 0.1s;
">
${btnText}
<span class="material-icons-round">open_in_new</span>
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('btn-error-redirect').addEventListener('click', () => {
window.location.href = url;
});
requestAnimationFrame(() => {
modal.classList.remove('hidden');
});
}

View File

@@ -1105,3 +1105,93 @@ body {
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3); box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
} }
} }
/* Day Header Badges */
.day-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.menu-code-badge {
font-size: 0.75rem;
font-weight: 700;
color: #8b5cf6;
/* Violet 500 */
background-color: rgba(139, 92, 246, 0.15);
border: 1px solid rgba(139, 92, 246, 0.3);
padding: 2px 6px;
border-radius: 6px;
line-height: normal;
display: inline-block;
}
/* Detailed Badge Colors */
.nav-badge.badge-violet {
background-color: #8b5cf6;
}
.nav-badge.badge-green {
background-color: var(--success-color);
}
.nav-badge.badge-red {
background-color: var(--error-color);
}
.nav-badge.badge-blue {
background-color: var(--accent-color);
}
/* Day Header Status Colors (User Request) */
.card-header.header-violet {
background-color: rgba(139, 92, 246, 0.15);
border-bottom: 2px solid #8b5cf6;
}
.card-header.header-green {
background-color: rgba(16, 185, 129, 0.15);
border-bottom: 2px solid var(--success-color);
}
.card-header.header-red {
background-color: rgba(239, 68, 68, 0.15);
border-bottom: 2px solid var(--error-color);
}
.card-header.header-violet .day-name,
.card-header.header-green .day-name,
.card-header.header-red .day-name {
font-weight: 700;
color: var(--text-primary);
/* Ensure text remains standard color */
}
/* Update Icon */
.update-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
background-color: rgba(16, 185, 129, 0.2); /* Green tint */
color: var(--success-color);
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
animation: pulse 2s infinite;
}
.update-icon:hover {
background-color: var(--success-color);
color: white;
transform: scale(1.1);
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}

1
version.txt Executable file
View File

@@ -0,0 +1 @@
v1.0.3