Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bab54fdf2d | |||
| efdb50083e | |||
| d82dd31bed | |||
| d6a2236a5b |
@@ -1,15 +1,17 @@
|
|||||||
# Kantine Wrapper Bookmarklet (v1.7.0)
|
# Kantine Wrapper Bookmarklet (v1.2.0)
|
||||||
|
|
||||||
Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability.
|
Ein intelligentes Bookmarklet für die Mitarbeiter-Kantine der Bessa App. Dieses Skript erweitert die Standardansicht um eine **Wochenübersicht**, Kostenkontrolle und verbesserte Usability.
|
||||||
|
|
||||||
## 🚀 Features
|
## 🚀 Features
|
||||||
|
|
||||||
* **Wochenübersicht:** Zeigt alle Tage der aktuellen Woche auf einen Blick.
|
* **Wochenübersicht:** Zeigt alle Tage der aktuellen Woche auf einen Blick.
|
||||||
|
* **Bestell-Countdown:** ⏳ Roter Alarm 1h vor Bestellschluss.
|
||||||
|
* **Smart Highlights:** 🌟 Markiere deine Favoriten (z.B. "Schnitzel", "Vegetarisch").
|
||||||
* **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).
|
||||||
* **API Fallback:** Prüft die Verbindung und bietet bei Fehlern einen Direktlink zur Originalseite.
|
|
||||||
* **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header.
|
* **Menu Badges:** Zeigt Menü-Codes (M1, M2+) direkt im Header.
|
||||||
|
* **Changelog:** Übersicht über neue Funktionen direkt im Installer.
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
|
|||||||
@@ -108,42 +108,108 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🍽️ Kantine Wrapper <span style="font-size:0.5em; opacity:0.6; font-weight:400; vertical-align:middle; margin-left:10px;">$VERSION</span></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">
|
|
||||||
<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:#029AA8">web.bessa.app/knapp-kantine</a></li>
|
|
||||||
<li>Klicke auf das Lesezeichen <code>Kantine $VERSION</code></li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h2>✨ Features</h2>
|
<!-- 1. BUTTON (Top Priority) -->
|
||||||
<ul>
|
<div class="card" style="text-align: center; border: 2px solid #029AA8;">
|
||||||
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
|
<p style="margin-bottom:15px; font-weight:bold;">👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
|
||||||
<li>💰 <strong>Kostenkontrolle:</strong> Automatische Berechnung der Wochensumme.</li>
|
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
|
||||||
<li>🔑 <strong>Auto-Login:</strong> Nutzt deine bestehende Session.</li>
|
</div>
|
||||||
<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;">
|
<!-- 2. INSTRUCTIONS -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>So funktioniert's</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Ziehe den Button oben in deine <strong>Lesezeichen-Leiste</strong> (Drag & Drop)</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 $VERSION</code></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. FEATURES -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>✨ Features</h2>
|
||||||
|
<ul>
|
||||||
|
<li>📅 <strong>Wochenübersicht:</strong> Die ganze Woche auf einen Blick.</li>
|
||||||
|
<li>⏳ <strong>Order Countdown:</strong> Roter Alarm 1h vor Bestellschluss.</li>
|
||||||
|
<li>🌟 <strong>Smart Highlights:</strong> Markiere deine Favoriten (z.B. "Schnitzel").</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>
|
||||||
|
</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>
|
<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.
|
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>
|
||||||
|
|
||||||
|
<!-- 4. CHANGELOG (Bottom) -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>Changelog</h2>
|
||||||
|
<div class="changelog-container">
|
||||||
|
<!-- CHANGELOG_PLACEHOLDER -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>👇 Diesen Button in die Lesezeichen-Leiste ziehen:</p>
|
|
||||||
<p><a class="bookmarklet" id="bookmarklet-link" href="#">⏳ Wird generiert...</a></p>
|
|
||||||
<script>
|
<script>
|
||||||
INSTALLEOF
|
INSTALLEOF
|
||||||
|
|
||||||
|
# Generate Changlog HTML from Markdown
|
||||||
|
CHANGELOG_HTML=""
|
||||||
|
if [ -f "$SCRIPT_DIR/changelog.md" ]; then
|
||||||
|
CHANGELOG_HTML=$(cat "$SCRIPT_DIR/changelog.md" | python3 -c "
|
||||||
|
import sys, re
|
||||||
|
md = sys.stdin.read()
|
||||||
|
# Convert headers to h3/h4
|
||||||
|
html = re.sub(r'^## (.*)', r'<h3>\1</h3>', md, flags=re.MULTILINE)
|
||||||
|
# Convert bullets to list items
|
||||||
|
html = re.sub(r'^- (.*)', r'<li>\1</li>', html, flags=re.MULTILINE)
|
||||||
|
# Wrap lists (simple heuristic)
|
||||||
|
html = html.replace('</h3>\n<li>', '</h3>\n<ul>\n<li>').replace('</li>\n<h', '</li>\n</ul>\n<h').replace('</li>\n\n', '</li>\n</ul>\n')
|
||||||
|
if '<li>' in html and '<ul>' not in html: html = '<ul>' + html + '</ul>'
|
||||||
|
print(html)
|
||||||
|
")
|
||||||
|
fi
|
||||||
|
|
||||||
# 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"
|
||||||
echo "$JS_CONTENT" | python3 -c "
|
echo "$JS_CONTENT" | python3 -c "
|
||||||
import sys, json
|
import sys, json, urllib.parse
|
||||||
js = sys.stdin.read()
|
|
||||||
css = open('$CSS_FILE').read().replace('\\n', ' ').replace(' ', ' ')
|
# 1. Read JS and Replace VERSION
|
||||||
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);})();'''
|
js_template = sys.stdin.read()
|
||||||
print(json.dumps(bmk) + ';')
|
js = js_template.replace('{{VERSION}}', '$VERSION')
|
||||||
" 2>/dev/null >> "$DIST_DIR/install.html" || echo "'javascript:alert(\"Build error\")'" >> "$DIST_DIR/install.html"
|
|
||||||
|
# 2. Prepare CSS for injection via createElement('style')
|
||||||
|
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
|
||||||
|
escaped_css = css.replace('\\\\', '\\\\\\\\').replace(\"'\", \"\\\\'\").replace('\"', '\\\\\"')
|
||||||
|
|
||||||
|
# 3. Update URL
|
||||||
|
update_url = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html'
|
||||||
|
js = js.replace('https://github.com/TauNeutrino/kantine-overview/raw/main/dist/install.html', update_url)
|
||||||
|
|
||||||
|
# 4. Create Bookmarklet Code with CSS injection
|
||||||
|
# Inject CSS via style element (same pattern as bookmarklet-payload.js)
|
||||||
|
css_injection = \"var s=document.createElement('style');s.textContent='\" + escaped_css + \"';document.head.appendChild(s);\"
|
||||||
|
bookmarklet_code = 'javascript:(function(){' + css_injection + js + '})();'
|
||||||
|
|
||||||
|
# 5. URL Encode
|
||||||
|
encoded_code = urllib.parse.quote(bookmarklet_code, safe=':/()!;=+,')
|
||||||
|
|
||||||
|
# Output as JSON string for the HTML script to assign to href
|
||||||
|
print(json.dumps(encoded_code) + ';')
|
||||||
|
" >> "$DIST_DIR/install.html"
|
||||||
|
|
||||||
|
# Inject Changelog into Installer HTML (Safe Python replace)
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
html = open('$DIST_DIR/install.html').read()
|
||||||
|
changelog = sys.stdin.read()
|
||||||
|
html = html.replace('<!-- CHANGELOG_PLACEHOLDER -->', changelog)
|
||||||
|
open('$DIST_DIR/install.html', 'w').write(html)
|
||||||
|
" << EOF
|
||||||
|
$CHANGELOG_HTML
|
||||||
|
EOF
|
||||||
|
|
||||||
cat >> "$DIST_DIR/install.html" << INSTALLEOF
|
cat >> "$DIST_DIR/install.html" << INSTALLEOF
|
||||||
document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
|
document.getElementById('bookmarklet-link').textContent = 'Kantine $VERSION';
|
||||||
@@ -157,3 +223,14 @@ echo ""
|
|||||||
echo "=== Build Complete ==="
|
echo "=== Build Complete ==="
|
||||||
echo "Files in $DIST_DIR:"
|
echo "Files in $DIST_DIR:"
|
||||||
ls -la "$DIST_DIR/"
|
ls -la "$DIST_DIR/"
|
||||||
|
|
||||||
|
# === 4. Run build-time tests ===
|
||||||
|
echo ""
|
||||||
|
echo "=== Running Build Tests ==="
|
||||||
|
python3 "$SCRIPT_DIR/test_build.py"
|
||||||
|
TEST_EXIT=$?
|
||||||
|
if [ $TEST_EXIT -ne 0 ]; then
|
||||||
|
echo "❌ Build tests FAILED! See above for details."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All build tests passed."
|
||||||
|
|||||||
26
changelog.md
Executable file
26
changelog.md
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
## v1.2.0 (2026-02-16)
|
||||||
|
- **Feature**: Bessere UX im Installer (Button oben, Log unten, Features aktualisiert). 💅
|
||||||
|
- **Tech**: Build-Tests hinzugefügt. 🧪
|
||||||
|
- **Fix**: Encoding-Probleme final behoben (dank Python Buildlogic). 🐍
|
||||||
|
|
||||||
|
## v1.1.2 (2026-02-16)
|
||||||
|
- **Fix**: Encoding-Problem beim Bookmarklet behoben (URL Malformed Error). 🔗
|
||||||
|
|
||||||
|
## v1.1.1 (2026-02-16)
|
||||||
|
- **Fix**: Kritischer Fehler behoben, der das Laden des Wrappers verhinderte. 🐛
|
||||||
|
|
||||||
|
## v1.1.0 (2026-02-16)
|
||||||
|
- **Feature: Bestell-Countdown**: Zeigt 1 Stunde vor Bestellschluss einen roten Countdown an. ⏳
|
||||||
|
- **Feature: Smart Highlights**: Markiere deine Lieblingsspeisen (z.B. "Schnitzel", "Vegetarisch"), damit sie leuchten. 🌟
|
||||||
|
- **Feature: Changelog**: Diese Übersicht der Änderungen. 📜
|
||||||
|
- **Verbesserung**: Live-Check der Version beim Update.
|
||||||
|
|
||||||
|
## v1.0.3 (2026-02-13)
|
||||||
|
- **Fix**: Update-Link öffnet nun den Installer direkt als Webseite (via htmlpreview).
|
||||||
|
|
||||||
|
## v1.0.2 (2026-02-13)
|
||||||
|
- **Sync**: Version mit GitHub synchronisiert.
|
||||||
|
|
||||||
|
## v1.0.1 (2026-02-12)
|
||||||
|
- **UI**: Besseres Design für "Nächste Woche" (Badges).
|
||||||
|
- **Core**: Grundlegende Funktionen (Bestellen, Guthaben, Token-Store).
|
||||||
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
96
dist/install.html
vendored
96
dist/install.html
vendored
File diff suppressed because one or more lines are too long
465
dist/kantine-standalone.html
vendored
465
dist/kantine-standalone.html
vendored
@@ -152,17 +152,6 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-cost {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--success-color);
|
|
||||||
background-color: var(--bg-body);
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-week-title {
|
.header-week-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -1177,13 +1166,15 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
/* Ensure text remains standard color */
|
/* Ensure text remains standard color */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update Icon */
|
/* Update Icon */
|
||||||
.update-icon {
|
.update-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
background-color: rgba(16, 185, 129, 0.2); /* Green tint */
|
background-color: rgba(16, 185, 129, 0.2);
|
||||||
|
/* Green tint */
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -1202,11 +1193,171 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
|
0% {
|
||||||
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
|
||||||
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
}
|
||||||
|
|
||||||
|
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>
|
|
||||||
|
/* Order Countdown */
|
||||||
|
#order-countdown {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#order-countdown span {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#order-countdown.urgent {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
|
color: #ef4444;
|
||||||
|
animation: pulse-red 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smart Highlights */
|
||||||
|
.highlight-glow {
|
||||||
|
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
|
||||||
|
/* Blue glow */
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.8);
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav Badge with Count */
|
||||||
|
.nav-badge.has-highlights {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
/* Neutral background */
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-badge .highlight-count {
|
||||||
|
color: #3b82f6;
|
||||||
|
/* Blue 500 */
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag Management Modal */
|
||||||
|
#tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
margin-left: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg-body);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update Banner Enhanced */
|
||||||
|
.change-summary {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Installer Changelog */
|
||||||
|
.changelog-container ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-container li {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-container h3 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--accent-color);
|
||||||
|
} </style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
@@ -1278,7 +1429,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.0.3</small></h1>
|
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.0</small></h1>
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1290,6 +1441,9 @@ body {
|
|||||||
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
||||||
<span class="material-icons-round">refresh</span>
|
<span class="material-icons-round">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
|
||||||
|
<span class="material-icons-round">label</span>
|
||||||
|
</button>
|
||||||
<div class="nav-group">
|
<div class="nav-group">
|
||||||
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
||||||
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
||||||
@@ -1356,6 +1510,27 @@ body {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="highlights-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Meine Highlights</h2>
|
||||||
|
<button id="btn-highlights-close" class="icon-btn" aria-label="Close">
|
||||||
|
<span class="material-icons-round">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p style="margin-bottom: 1rem; color: var(--text-secondary);">
|
||||||
|
Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.
|
||||||
|
</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch...">
|
||||||
|
<button id="btn-add-tag" class="btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
<div id="tags-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div id="last-updated-banner" class="banner hidden">
|
<div id="last-updated-banner" class="banner hidden">
|
||||||
<span class="material-icons-round">update</span>
|
<span class="material-icons-round">update</span>
|
||||||
@@ -1386,6 +1561,41 @@ body {
|
|||||||
const loginForm = document.getElementById('login-form');
|
const loginForm = document.getElementById('login-form');
|
||||||
const loginModal = document.getElementById('login-modal');
|
const loginModal = document.getElementById('login-modal');
|
||||||
|
|
||||||
|
// Highlights Modal
|
||||||
|
const btnHighlights = document.getElementById('btn-highlights');
|
||||||
|
const highlightsModal = document.getElementById('highlights-modal');
|
||||||
|
const btnHighlightsClose = document.getElementById('btn-highlights-close');
|
||||||
|
const btnAddTag = document.getElementById('btn-add-tag');
|
||||||
|
const tagInput = document.getElementById('tag-input');
|
||||||
|
|
||||||
|
btnHighlights.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.remove('hidden');
|
||||||
|
renderTagsList();
|
||||||
|
tagInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnHighlightsClose.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnAddTag.addEventListener('click', () => {
|
||||||
|
const tag = tagInput.value;
|
||||||
|
if (addHighlightTag(tag)) {
|
||||||
|
tagInput.value = '';
|
||||||
|
renderTagsList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tagInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
btnAddTag.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
@@ -1804,18 +2014,65 @@ body {
|
|||||||
}
|
}
|
||||||
// Refresh menu data to update UI
|
// Refresh menu data to update UI
|
||||||
loadMenuDataFromAPI();
|
loadMenuDataFromAPI();
|
||||||
break; // One refresh is enough
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Poll error for ${flagId}:`, err);
|
console.error(`Poll error for ${flagId}:`, err);
|
||||||
|
// Small delay between checks
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between checks
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Highlight Management ===
|
||||||
|
let highlightTags = JSON.parse(localStorage.getItem('kantine_highlightTags') || '[]');
|
||||||
|
|
||||||
|
function saveHighlightTags() {
|
||||||
|
localStorage.setItem('kantine_highlightTags', JSON.stringify(highlightTags));
|
||||||
|
renderVisibleWeeks(); // Refresh UI to apply changes
|
||||||
|
updateNextWeekBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHighlightTag(tag) {
|
||||||
|
tag = tag.trim().toLowerCase();
|
||||||
|
if (tag && !highlightTags.includes(tag)) {
|
||||||
|
highlightTags.push(tag);
|
||||||
|
saveHighlightTags();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHighlightTag(tag) {
|
||||||
|
highlightTags = highlightTags.filter(t => t !== tag);
|
||||||
|
saveHighlightTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsList() {
|
||||||
|
const list = document.getElementById('tags-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
highlightTags.forEach(tag => {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'tag-badge';
|
||||||
|
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}">×</span>`;
|
||||||
|
list.appendChild(badge);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind remove events
|
||||||
|
list.querySelectorAll('.tag-remove').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
removeHighlightTag(e.target.dataset.tag);
|
||||||
|
renderTagsList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHighlight(text) {
|
||||||
|
if (!text) return false;
|
||||||
|
text = text.toLowerCase();
|
||||||
|
return highlightTags.some(tag => text.includes(tag));
|
||||||
|
}
|
||||||
|
|
||||||
// === Local Menu Cache (localStorage) ===
|
// === Local Menu Cache (localStorage) ===
|
||||||
const CACHE_KEY = 'kantine_menuCache';
|
const CACHE_KEY = 'kantine_menuCache';
|
||||||
const CACHE_TS_KEY = 'kantine_menuCacheTs';
|
const CACHE_TS_KEY = 'kantine_menuCacheTs';
|
||||||
@@ -2140,6 +2397,25 @@ body {
|
|||||||
badge.classList.add('badge-blue'); // Default / partial state
|
badge.classList.add('badge-blue'); // Default / partial state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advanced Feature: Highlight Count
|
||||||
|
let highlightCount = 0;
|
||||||
|
if (nextWeekData && nextWeekData.days) {
|
||||||
|
nextWeekData.days.forEach(day => {
|
||||||
|
day.items.forEach(item => {
|
||||||
|
if (checkHighlight(item.name) || checkHighlight(item.description)) {
|
||||||
|
highlightCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlightCount > 0) {
|
||||||
|
// Append blue count
|
||||||
|
badge.innerHTML += `<span class="highlight-count" title="${highlightCount} Highlight Menüs">(${highlightCount})</span>`;
|
||||||
|
badge.title += ` • ${highlightCount} Highlights gefunden`;
|
||||||
|
badge.classList.add('has-highlights');
|
||||||
|
}
|
||||||
|
|
||||||
} else if (badge) {
|
} else if (badge) {
|
||||||
badge.remove();
|
badge.remove();
|
||||||
}
|
}
|
||||||
@@ -2486,9 +2762,8 @@ body {
|
|||||||
|
|
||||||
// === Version Check ===
|
// === Version Check ===
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
const CurrentVersion = 'v1.0.3'; // Injected by build script
|
const CurrentVersion = 'v1.2.0';
|
||||||
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
|
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';
|
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})`);
|
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
|
||||||
@@ -2498,36 +2773,145 @@ body {
|
|||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
|
|
||||||
const remoteVersion = (await response.text()).trim();
|
const remoteVersion = (await response.text()).trim();
|
||||||
console.log(`[Kantine] Remote version: ${remoteVersion}`);
|
|
||||||
|
|
||||||
if (remoteVersion && remoteVersion !== CurrentVersion) {
|
if (remoteVersion && remoteVersion !== CurrentVersion) {
|
||||||
// Simple semantic version check or string inequality
|
console.log(`[Kantine] New version available: ${remoteVersion}`);
|
||||||
// Assuming format v1.0.0
|
|
||||||
showUpdateIcon(remoteVersion, InstallerUrl);
|
// Fetch Changelog content
|
||||||
|
let changeSummary = '';
|
||||||
|
try {
|
||||||
|
const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');
|
||||||
|
if (clResp.ok) {
|
||||||
|
const clText = await clResp.text();
|
||||||
|
const match = clText.match(/## (v[^\n]+)\n((?:-[^\n]+\n)+)/);
|
||||||
|
if (match && match[1].includes(remoteVersion)) {
|
||||||
|
changeSummary = match[2].replace(/- /g, '• ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('No changelog', e); }
|
||||||
|
|
||||||
|
// Create Banner
|
||||||
|
const updateBanner = document.createElement('div');
|
||||||
|
updateBanner.className = 'update-banner';
|
||||||
|
updateBanner.innerHTML = `
|
||||||
|
<div class="update-content">
|
||||||
|
<strong>Update verfügbar: ${remoteVersion}</strong>
|
||||||
|
${changeSummary ? `<pre class="change-summary">${changeSummary}</pre>` : ''}
|
||||||
|
<a href="${InstallerUrl}" target="_blank" class="update-link">
|
||||||
|
<span class="material-icons-round">system_update_alt</span>
|
||||||
|
Jetzt aktualisieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button class="icon-btn-small close-update">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(updateBanner);
|
||||||
|
updateBanner.querySelector('.close-update').addEventListener('click', () => updateBanner.remove());
|
||||||
|
|
||||||
|
// Highlight Header Icon
|
||||||
|
const lastUpdatedIcon = document.querySelector('.material-icons-round.logo-icon');
|
||||||
|
if (lastUpdatedIcon) {
|
||||||
|
lastUpdatedIcon.style.color = 'var(--accent-color)';
|
||||||
|
lastUpdatedIcon.parentElement.title = `Update verfügbar: ${remoteVersion}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Kantine] Version check failed:', error);
|
console.warn('[Kantine] Version check failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showUpdateIcon(newVersion, url) {
|
// === Order Countdown ===
|
||||||
const headerTitle = document.querySelector('.header-left h1');
|
function updateCountdown() {
|
||||||
if (!headerTitle) return;
|
const now = new Date();
|
||||||
|
const currentDay = now.getDay();
|
||||||
|
// Skip weekends (0=Sun, 6=Sat)
|
||||||
|
if (currentDay === 0 || currentDay === 6) {
|
||||||
|
removeCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already added
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
if (headerTitle.querySelector('.update-icon')) return;
|
|
||||||
|
|
||||||
const icon = document.createElement('a');
|
// 1. Check if we already ordered for today
|
||||||
icon.className = 'update-icon';
|
let hasOrder = false;
|
||||||
icon.href = url;
|
// Optimization: Check orderMap for today's date
|
||||||
icon.target = '_blank';
|
// Keys are "YYYY-MM-DD_ArticleID"
|
||||||
icon.innerHTML = '🆕'; // User requested icon
|
for (const key of orderMap.keys()) {
|
||||||
icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`;
|
if (key.startsWith(todayStr)) {
|
||||||
|
hasOrder = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
headerTitle.appendChild(icon);
|
if (hasOrder) {
|
||||||
showToast(`Update verfügbar: ${newVersion}`, 'info');
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update countdown every minute
|
||||||
|
setInterval(updateCountdown, 60000);
|
||||||
|
// Also update on load
|
||||||
|
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()));
|
||||||
@@ -2543,6 +2927,7 @@ body {
|
|||||||
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;
|
||||||
|
|||||||
280
kantine.js
280
kantine.js
@@ -78,6 +78,9 @@
|
|||||||
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren" title="Menüdaten neu laden">
|
||||||
<span class="material-icons-round">refresh</span>
|
<span class="material-icons-round">refresh</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-highlights" class="icon-btn" aria-label="Persönliche Highlights verwalten" title="Persönliche Highlights verwalten">
|
||||||
|
<span class="material-icons-round">label</span>
|
||||||
|
</button>
|
||||||
<div class="nav-group">
|
<div class="nav-group">
|
||||||
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
||||||
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
||||||
@@ -144,6 +147,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="highlights-modal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>Meine Highlights</h2>
|
||||||
|
<button id="btn-highlights-close" class="icon-btn" aria-label="Close">
|
||||||
|
<span class="material-icons-round">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p style="margin-bottom: 1rem; color: var(--text-secondary);">
|
||||||
|
Markiere Menüs automatisch, wenn sie diese Schlagwörter enthalten.
|
||||||
|
</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="tag-input" placeholder="z.B. Schnitzel, Vegetarisch...">
|
||||||
|
<button id="btn-add-tag" class="btn-primary">Hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
<div id="tags-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<div id="last-updated-banner" class="banner hidden">
|
<div id="last-updated-banner" class="banner hidden">
|
||||||
<span class="material-icons-round">update</span>
|
<span class="material-icons-round">update</span>
|
||||||
@@ -174,6 +198,41 @@
|
|||||||
const loginForm = document.getElementById('login-form');
|
const loginForm = document.getElementById('login-form');
|
||||||
const loginModal = document.getElementById('login-modal');
|
const loginModal = document.getElementById('login-modal');
|
||||||
|
|
||||||
|
// Highlights Modal
|
||||||
|
const btnHighlights = document.getElementById('btn-highlights');
|
||||||
|
const highlightsModal = document.getElementById('highlights-modal');
|
||||||
|
const btnHighlightsClose = document.getElementById('btn-highlights-close');
|
||||||
|
const btnAddTag = document.getElementById('btn-add-tag');
|
||||||
|
const tagInput = document.getElementById('tag-input');
|
||||||
|
|
||||||
|
btnHighlights.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.remove('hidden');
|
||||||
|
renderTagsList();
|
||||||
|
tagInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
btnHighlightsClose.addEventListener('click', () => {
|
||||||
|
highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target === highlightsModal) highlightsModal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
btnAddTag.addEventListener('click', () => {
|
||||||
|
const tag = tagInput.value;
|
||||||
|
if (addHighlightTag(tag)) {
|
||||||
|
tagInput.value = '';
|
||||||
|
renderTagsList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tagInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
btnAddTag.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Theme
|
// Theme
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem('theme');
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
@@ -592,18 +651,65 @@
|
|||||||
}
|
}
|
||||||
// Refresh menu data to update UI
|
// Refresh menu data to update UI
|
||||||
loadMenuDataFromAPI();
|
loadMenuDataFromAPI();
|
||||||
break; // One refresh is enough
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Poll error for ${flagId}:`, err);
|
console.error(`Poll error for ${flagId}:`, err);
|
||||||
|
// Small delay between checks
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small delay between checks
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Highlight Management ===
|
||||||
|
let highlightTags = JSON.parse(localStorage.getItem('kantine_highlightTags') || '[]');
|
||||||
|
|
||||||
|
function saveHighlightTags() {
|
||||||
|
localStorage.setItem('kantine_highlightTags', JSON.stringify(highlightTags));
|
||||||
|
renderVisibleWeeks(); // Refresh UI to apply changes
|
||||||
|
updateNextWeekBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHighlightTag(tag) {
|
||||||
|
tag = tag.trim().toLowerCase();
|
||||||
|
if (tag && !highlightTags.includes(tag)) {
|
||||||
|
highlightTags.push(tag);
|
||||||
|
saveHighlightTags();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHighlightTag(tag) {
|
||||||
|
highlightTags = highlightTags.filter(t => t !== tag);
|
||||||
|
saveHighlightTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTagsList() {
|
||||||
|
const list = document.getElementById('tags-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
highlightTags.forEach(tag => {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'tag-badge';
|
||||||
|
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}">×</span>`;
|
||||||
|
list.appendChild(badge);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind remove events
|
||||||
|
list.querySelectorAll('.tag-remove').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
removeHighlightTag(e.target.dataset.tag);
|
||||||
|
renderTagsList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHighlight(text) {
|
||||||
|
if (!text) return false;
|
||||||
|
text = text.toLowerCase();
|
||||||
|
return highlightTags.some(tag => text.includes(tag));
|
||||||
|
}
|
||||||
|
|
||||||
// === Local Menu Cache (localStorage) ===
|
// === Local Menu Cache (localStorage) ===
|
||||||
const CACHE_KEY = 'kantine_menuCache';
|
const CACHE_KEY = 'kantine_menuCache';
|
||||||
const CACHE_TS_KEY = 'kantine_menuCacheTs';
|
const CACHE_TS_KEY = 'kantine_menuCacheTs';
|
||||||
@@ -928,6 +1034,25 @@
|
|||||||
badge.classList.add('badge-blue'); // Default / partial state
|
badge.classList.add('badge-blue'); // Default / partial state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advanced Feature: Highlight Count
|
||||||
|
let highlightCount = 0;
|
||||||
|
if (nextWeekData && nextWeekData.days) {
|
||||||
|
nextWeekData.days.forEach(day => {
|
||||||
|
day.items.forEach(item => {
|
||||||
|
if (checkHighlight(item.name) || checkHighlight(item.description)) {
|
||||||
|
highlightCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highlightCount > 0) {
|
||||||
|
// Append blue count
|
||||||
|
badge.innerHTML += `<span class="highlight-count" title="${highlightCount} Highlight Menüs">(${highlightCount})</span>`;
|
||||||
|
badge.title += ` • ${highlightCount} Highlights gefunden`;
|
||||||
|
badge.classList.add('has-highlights');
|
||||||
|
}
|
||||||
|
|
||||||
} else if (badge) {
|
} else if (badge) {
|
||||||
badge.remove();
|
badge.remove();
|
||||||
}
|
}
|
||||||
@@ -1274,9 +1399,8 @@
|
|||||||
|
|
||||||
// === Version Check ===
|
// === Version Check ===
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
const CurrentVersion = '{{VERSION}}'; // Injected by build script
|
const CurrentVersion = '{{VERSION}}';
|
||||||
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
|
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';
|
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})`);
|
console.log(`[Kantine] Checking for updates... (Current: ${CurrentVersion})`);
|
||||||
@@ -1286,36 +1410,145 @@
|
|||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
|
|
||||||
const remoteVersion = (await response.text()).trim();
|
const remoteVersion = (await response.text()).trim();
|
||||||
console.log(`[Kantine] Remote version: ${remoteVersion}`);
|
|
||||||
|
|
||||||
if (remoteVersion && remoteVersion !== CurrentVersion) {
|
if (remoteVersion && remoteVersion !== CurrentVersion) {
|
||||||
// Simple semantic version check or string inequality
|
console.log(`[Kantine] New version available: ${remoteVersion}`);
|
||||||
// Assuming format v1.0.0
|
|
||||||
showUpdateIcon(remoteVersion, InstallerUrl);
|
// Fetch Changelog content
|
||||||
|
let changeSummary = '';
|
||||||
|
try {
|
||||||
|
const clResp = await fetch('https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/changelog.md');
|
||||||
|
if (clResp.ok) {
|
||||||
|
const clText = await clResp.text();
|
||||||
|
const match = clText.match(/## (v[^\n]+)\n((?:-[^\n]+\n)+)/);
|
||||||
|
if (match && match[1].includes(remoteVersion)) {
|
||||||
|
changeSummary = match[2].replace(/- /g, '• ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('No changelog', e); }
|
||||||
|
|
||||||
|
// Create Banner
|
||||||
|
const updateBanner = document.createElement('div');
|
||||||
|
updateBanner.className = 'update-banner';
|
||||||
|
updateBanner.innerHTML = `
|
||||||
|
<div class="update-content">
|
||||||
|
<strong>Update verfügbar: ${remoteVersion}</strong>
|
||||||
|
${changeSummary ? `<pre class="change-summary">${changeSummary}</pre>` : ''}
|
||||||
|
<a href="${InstallerUrl}" target="_blank" class="update-link">
|
||||||
|
<span class="material-icons-round">system_update_alt</span>
|
||||||
|
Jetzt aktualisieren
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<button class="icon-btn-small close-update">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(updateBanner);
|
||||||
|
updateBanner.querySelector('.close-update').addEventListener('click', () => updateBanner.remove());
|
||||||
|
|
||||||
|
// Highlight Header Icon
|
||||||
|
const lastUpdatedIcon = document.querySelector('.material-icons-round.logo-icon');
|
||||||
|
if (lastUpdatedIcon) {
|
||||||
|
lastUpdatedIcon.style.color = 'var(--accent-color)';
|
||||||
|
lastUpdatedIcon.parentElement.title = `Update verfügbar: ${remoteVersion}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Kantine] Version check failed:', error);
|
console.warn('[Kantine] Version check failed:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showUpdateIcon(newVersion, url) {
|
// === Order Countdown ===
|
||||||
const headerTitle = document.querySelector('.header-left h1');
|
function updateCountdown() {
|
||||||
if (!headerTitle) return;
|
const now = new Date();
|
||||||
|
const currentDay = now.getDay();
|
||||||
|
// Skip weekends (0=Sun, 6=Sat)
|
||||||
|
if (currentDay === 0 || currentDay === 6) {
|
||||||
|
removeCountdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already added
|
const todayStr = now.toISOString().split('T')[0];
|
||||||
if (headerTitle.querySelector('.update-icon')) return;
|
|
||||||
|
|
||||||
const icon = document.createElement('a');
|
// 1. Check if we already ordered for today
|
||||||
icon.className = 'update-icon';
|
let hasOrder = false;
|
||||||
icon.href = url;
|
// Optimization: Check orderMap for today's date
|
||||||
icon.target = '_blank';
|
// Keys are "YYYY-MM-DD_ArticleID"
|
||||||
icon.innerHTML = '🆕'; // User requested icon
|
for (const key of orderMap.keys()) {
|
||||||
icon.title = `Neue Version verfügbar (${newVersion}). Klick für download`;
|
if (key.startsWith(todayStr)) {
|
||||||
|
hasOrder = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
headerTitle.appendChild(icon);
|
if (hasOrder) {
|
||||||
showToast(`Update verfügbar: ${newVersion}`, 'info');
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update countdown every minute
|
||||||
|
setInterval(updateCountdown, 60000);
|
||||||
|
// Also update on load
|
||||||
|
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()));
|
||||||
@@ -1331,6 +1564,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;
|
||||||
|
|||||||
182
style.css
182
style.css
@@ -141,17 +141,6 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-cost {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--success-color);
|
|
||||||
background-color: var(--bg-body);
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-week-title {
|
.header-week-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -1166,13 +1155,15 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
/* Ensure text remains standard color */
|
/* Ensure text remains standard color */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update Icon */
|
/* Update Icon */
|
||||||
.update-icon {
|
.update-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
background-color: rgba(16, 185, 129, 0.2); /* Green tint */
|
background-color: rgba(16, 185, 129, 0.2);
|
||||||
|
/* Green tint */
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
@@ -1191,7 +1182,168 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
|
0% {
|
||||||
70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4);
|
||||||
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order Countdown */
|
||||||
|
#order-countdown {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#order-countdown span {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#order-countdown.urgent {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
|
color: #ef4444;
|
||||||
|
animation: pulse-red 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-red {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smart Highlights */
|
||||||
|
.highlight-glow {
|
||||||
|
box-shadow: 0 0 15px rgba(59, 130, 246, 0.5);
|
||||||
|
/* Blue glow */
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.8);
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav Badge with Count */
|
||||||
|
.nav-badge.has-highlights {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
/* Neutral background */
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-badge .highlight-count {
|
||||||
|
color: #3b82f6;
|
||||||
|
/* Blue 500 */
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag Management Modal */
|
||||||
|
#tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #3b82f6;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove {
|
||||||
|
margin-left: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg-body);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update Banner Enhanced */
|
||||||
|
.change-summary {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Installer Changelog */
|
||||||
|
.changelog-container ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-container li {
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-container h3 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
89
test_build.py
Executable file
89
test_build.py
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DIST_DIR = os.path.join(os.path.dirname(__file__), 'dist')
|
||||||
|
INSTALL_HTML = os.path.join(DIST_DIR, 'install.html')
|
||||||
|
BOOKMARKLET_TXT = os.path.join(DIST_DIR, 'bookmarklet.txt')
|
||||||
|
STANDALONE_HTML = os.path.join(DIST_DIR, 'kantine-standalone.html')
|
||||||
|
|
||||||
|
def check_file_exists(path, description):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"❌ MISSING: {description} ({path})")
|
||||||
|
return False
|
||||||
|
# Check if not empty
|
||||||
|
if os.path.getsize(path) == 0:
|
||||||
|
print(f"❌ EMPTY: {description} ({path})")
|
||||||
|
return False
|
||||||
|
print(f"✅ FOUND: {description}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_content(path, must_contain=[], must_not_contain=[]):
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
success = True
|
||||||
|
for item in must_contain:
|
||||||
|
if item not in content:
|
||||||
|
print(f"❌ MISSING CONTENT: '{item}' in {os.path.basename(path)}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
for item in must_not_contain:
|
||||||
|
if item in content:
|
||||||
|
print(f"❌ FORBIDDEN CONTENT: '{item}' in {os.path.basename(path)}")
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ CONTENT VERIFIED: {os.path.basename(path)}")
|
||||||
|
return success
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== Running Build Tests ===")
|
||||||
|
|
||||||
|
# 1. Existence Check
|
||||||
|
if not all([
|
||||||
|
check_file_exists(INSTALL_HTML, "Installer HTML"),
|
||||||
|
check_file_exists(BOOKMARKLET_TXT, "Bookmarklet Text"),
|
||||||
|
check_file_exists(STANDALONE_HTML, "Standalone HTML")
|
||||||
|
]):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 2. Bookmarklet Logic Check
|
||||||
|
# Must have the CSS injection fix from the external AI
|
||||||
|
# Must have correct versioning
|
||||||
|
# Must be properly URL encoded (checking for common issues)
|
||||||
|
|
||||||
|
# Read bookmarklet code (decoded mostly by being in txt? No, txt is usually the raw URL)
|
||||||
|
with open(BOOKMARKLET_TXT, 'r') as f:
|
||||||
|
bm_code = f.read().strip()
|
||||||
|
|
||||||
|
if not bm_code.startswith("javascript:"):
|
||||||
|
print("❌ Bookmarklet does not start with 'javascript:'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check for placeholder leftovers
|
||||||
|
if not check_content(BOOKMARKLET_TXT,
|
||||||
|
must_contain=["document.createElement('style')", "M1", "M2"],
|
||||||
|
must_not_contain=["{{VERSION}}", "{{CSS_ESCAPED}}"]):
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check for CSS injection specific logic
|
||||||
|
if "document.head.appendChild(s)" not in bm_code and "appendChild(s)" not in bm_code: # URL encoded might mask this, strictly checking decoded would be better but simple check first
|
||||||
|
# Actually bm_code is URL encoded. We should decode it to verify logic.
|
||||||
|
import urllib.parse
|
||||||
|
decoded = urllib.parse.unquote(bm_code)
|
||||||
|
if "document.createElement('style')" not in decoded:
|
||||||
|
print("❌ CSS Injection logic missing in bookmarklet")
|
||||||
|
sys.exit(1)
|
||||||
|
print("✅ CSS Injection logic confirmed")
|
||||||
|
|
||||||
|
# 3. Installer Check
|
||||||
|
if not check_content(INSTALL_HTML,
|
||||||
|
must_contain=["Kantine Wrapper", "So funktioniert's", "changelog-container"],
|
||||||
|
must_not_contain=["CHANGELOG_HTML_PLACEHOLDER"]): # If we used that
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("🎉 ALL TESTS PASSED")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1 +1 @@
|
|||||||
v1.0.3
|
v1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user