Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b8ac5ca1d | ||
|
|
2964eba88b | ||
|
|
089e5375b4 | ||
|
|
e514f42dbe | ||
|
|
f7a9b061ea | ||
|
|
fe75347682 | ||
|
|
5c5255058c | ||
|
|
e16398ef74 | ||
|
|
6a57e2716f | ||
|
|
d383808f4f | ||
|
|
498b400033 | ||
|
|
884a17e7d3 | ||
|
|
ad660799ec | ||
|
|
a5fe50bbf0 | ||
|
|
a98faec51e | ||
|
|
212bf3b015 | ||
|
|
f29ecd4b79 | ||
|
|
1be6e44d7f | ||
|
|
49b0ab17ac | ||
|
|
55e738a554 | ||
|
|
45adfa9d5d | ||
|
|
f5f6dddba3 | ||
|
|
b06f6c3551 | ||
|
|
b66030dce5 | ||
|
|
8e7ec468d4 | ||
|
|
8ce3ae4c92 |
@@ -28,13 +28,13 @@ if [ ! -f "$JS_FILE" ]; then echo "ERROR: $JS_FILE not found"; exit 1; fi
|
|||||||
# Generate favicon.png from favicon_base.png if base exists
|
# Generate favicon.png from favicon_base.png if base exists
|
||||||
FAVICON_BASE="$SCRIPT_DIR/favicon_base.png"
|
FAVICON_BASE="$SCRIPT_DIR/favicon_base.png"
|
||||||
if [ -f "$FAVICON_BASE" ]; then
|
if [ -f "$FAVICON_BASE" ]; then
|
||||||
echo "Generating 32x32 favicon.png from favicon_base.png..."
|
echo "Generating 40x40 favicon.png from favicon_base.png..."
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import sys
|
import sys
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
try:
|
try:
|
||||||
img = Image.open('$FAVICON_BASE')
|
img = Image.open('$FAVICON_BASE')
|
||||||
img_resized = img.resize((32, 32), Image.Resampling.LANCZOS)
|
img_resized = img.resize((40, 40), Image.Resampling.LANCZOS)
|
||||||
img_resized.save('$FAVICON_FILE')
|
img_resized.save('$FAVICON_FILE')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Favicon generation error:', e)
|
print('Favicon generation error:', e)
|
||||||
@@ -99,15 +99,20 @@ echo "✅ Standalone HTML: $DIST_DIR/kantine-standalone.html"
|
|||||||
# Escape CSS for embedding in JS string
|
# Escape CSS for embedding in JS string
|
||||||
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
|
CSS_ESCAPED=$(echo "$CSS_CONTENT" | sed "s/'/\\\\'/g" | tr '\n' ' ' | sed 's/ */ /g')
|
||||||
|
|
||||||
# Build bookmarklet payload
|
# Create a minified version for the injected bookmarklet payloads
|
||||||
|
echo "Minifying JS with Terser..."
|
||||||
|
TEMP_JS=$(mktemp)
|
||||||
|
echo "$JS_CONTENT" > "$TEMP_JS"
|
||||||
|
JS_MINIFIED=$(npx -y terser "$TEMP_JS" --compress --mangle)
|
||||||
|
rm -f "$TEMP_JS"
|
||||||
|
|
||||||
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
|
cat > "$DIST_DIR/bookmarklet-payload.js" << PAYLOADEOF
|
||||||
(function(){
|
javascript:(function(){
|
||||||
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
|
if(window.__KANTINE_LOADED){alert('Kantine Wrapper already loaded!');return;}
|
||||||
var s=document.createElement('style');
|
var s=document.createElement('style');s.textContent='${CSS_ESCAPED}';document.head.appendChild(s);
|
||||||
s.textContent='${CSS_ESCAPED}';
|
// Inject JS logic
|
||||||
document.head.appendChild(s);
|
|
||||||
var sc=document.createElement('script');
|
var sc=document.createElement('script');
|
||||||
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/$/"/');
|
sc.textContent=$(echo "$JS_MINIFIED" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null || echo "$JS_MINIFIED" | 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
|
||||||
@@ -161,7 +166,7 @@ cat > "$DIST_DIR/install.html" << INSTALLEOF
|
|||||||
|
|
||||||
<div style="text-align: center; margin-bottom: 30px;">
|
<div style="text-align: center; margin-bottom: 30px;">
|
||||||
<h1 style="margin-bottom: 5px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
<h1 style="margin-bottom: 5px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||||||
<img src="$FAVICON_URL" alt="Logo" style="width: 38px; height: 38px;">
|
<img src="$FAVICON_URL" alt="Logo" style="width: 40px; height: 40px;">
|
||||||
Kantine Wrapper
|
Kantine Wrapper
|
||||||
<span style="font-size:0.5em; opacity:0.6; font-weight:400; margin-left:5px;">$VERSION</span>
|
<span style="font-size:0.5em; opacity:0.6; font-weight:400; margin-left:5px;">$VERSION</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -239,12 +244,11 @@ 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_MINIFIED" | python3 -c "
|
||||||
import sys, json, urllib.parse
|
import sys, json, urllib.parse
|
||||||
|
|
||||||
# 1. Read JS and Replace VERSION + Favicon
|
# 1. Read JS and Replace VERSION + Favicon
|
||||||
js_template = sys.stdin.read()
|
js = sys.stdin.read()
|
||||||
js = js_template.replace('{{VERSION}}', '$VERSION').replace('{{FAVICON_DATA_URI}}', '$FAVICON_URL')
|
|
||||||
|
|
||||||
# 2. Prepare CSS for injection via createElement('style')
|
# 2. Prepare CSS for injection via createElement('style')
|
||||||
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
|
css = open('$CSS_FILE').read().replace('\n', ' ').replace(' ', ' ')
|
||||||
@@ -303,26 +307,30 @@ ls -la "$DIST_DIR/"
|
|||||||
# === 4. Run build-time tests ===
|
# === 4. Run build-time tests ===
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Running Logic Tests ==="
|
echo "=== Running Logic Tests ==="
|
||||||
node "$SCRIPT_DIR/test_logic.js"
|
timeout 15s node "$SCRIPT_DIR/test_logic.js"
|
||||||
LOGIC_EXIT=$?
|
LOGIC_EXIT=$?
|
||||||
if [ $LOGIC_EXIT -ne 0 ]; then
|
if [ $LOGIC_EXIT -ne 0 ]; then
|
||||||
echo "❌ Logic tests FAILED! See above for details."
|
echo "❌ Logic tests FAILED or TIMED OUT (Exit: $LOGIC_EXIT)! See above for details."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== Running DOM Interaction Tests ==="
|
echo "=== Running DOM Interaction Tests ==="
|
||||||
node "$SCRIPT_DIR/tests/test_dom.js"
|
timeout 15s node "$SCRIPT_DIR/tests/test_dom.js"
|
||||||
DOM_EXIT=$?
|
DOM_EXIT=$?
|
||||||
if [ $DOM_EXIT -ne 0 ]; then
|
if [ $DOM_EXIT -ne 0 ]; then
|
||||||
echo "❌ DOM UI tests FAILED! Regressions detected."
|
echo "❌ DOM UI tests FAILED or TIMED OUT (Exit: $DOM_EXIT)! Regressions detected."
|
||||||
|
# Ensure playwright processes are killed if they leak
|
||||||
|
pkill -f playwright || true
|
||||||
|
pkill -f "node.*test_dom" || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
echo "=== Running Build Tests ==="
|
echo "=== Running Build Tests ==="
|
||||||
python3 "$SCRIPT_DIR/test_build.py"
|
timeout 15s python3 "$SCRIPT_DIR/test_build.py"
|
||||||
TEST_EXIT=$?
|
TEST_EXIT=$?
|
||||||
if [ $TEST_EXIT -ne 0 ]; then
|
if [ $TEST_EXIT -ne 0 ]; then
|
||||||
echo "❌ Build tests FAILED! See above for details."
|
echo "❌ Build tests FAILED or TIMED OUT (Exit: $TEST_EXIT)! See above for details."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "✅ All build tests passed."
|
echo "✅ All build tests passed."
|
||||||
|
|||||||
23
changelog.md
23
changelog.md
@@ -1,3 +1,26 @@
|
|||||||
|
## v1.6.8 (2026-03-06)
|
||||||
|
- ⚡ **Performance**: Das JavaScript für das Kantinen-Bookmarklet wird nun beim Build-Prozess (via Terser) minimiert, was die Länge der injizierten URL spürbar reduziert.
|
||||||
|
|
||||||
|
## v1.6.7 (2026-03-06)
|
||||||
|
- 🎨 **Style**: Das neue Header-Logo (`favicon_base.png`) wird nun konsequent auf 40x40px generiert und gerendert.
|
||||||
|
|
||||||
|
## v1.6.6 (2026-03-06)
|
||||||
|
- 🎨 **Style**: Den Schatten und den hervorstehenden Karten-Effekt für bestellte Menüs an vergangenen Tagen komplett entfernt - verbleiben nun visuell flach und unaufdringlich wie nicht-bestellte Menüs.
|
||||||
|
|
||||||
|
## v1.6.5 (2026-03-06)
|
||||||
|
- ✨ **Feature**: Das `restaurant_menu` Icon im Header wurde durch das neue `favicon_base.png` Logo ersetzt, passend zur Textgröße skaliert.
|
||||||
|
- 🎨 **Style**: Violette Umrahmung (Bestellt-Markierung) an vergangenen Tagen entfernt, um den Fokus auf aktuelle und zukünftige Bestellungen zu lenken.
|
||||||
|
- 🎨 **Style**: Der Glow-Effekt für am heutigen Tag bestellte Menüs wurde intensiviert.
|
||||||
|
|
||||||
|
## v1.6.4 (2026-03-05)
|
||||||
|
- ✨ **Feature**: Sprach-Lexikon (DE/EN) massiv erweitert um österreichische Begriffe (Nockerl, Fleckerl, Topfen, Mohn, Most etc.) und gängige Tippfehler aus dem Bessa-System (trukey, coffe, oveb etc.).
|
||||||
|
- 🧹 **Cleanup**: Sprach-Lexikon dedupliziert und alphabetisch sortiert für bessere Performance und Wartbarkeit.
|
||||||
|
- 🐛 **Bugfix**: Trennung von zweisprachigen Menüs (`splitLanguage`) verbessert: Erfasst nun auch Schrägstriche ohne Leerzeichen (z.B. `Suppe/Soup`).
|
||||||
|
- 🐛 **Bugfix**: Fehlerhafte Badge-Anzeige korrigiert (Variable `count` vs `orderCount`).
|
||||||
|
|
||||||
|
## v1.6.3 (2026-03-05)
|
||||||
|
- ✨ **Chore**: Slogan im Footer aktualisiert ("Jetzt Bessa Einfach! • Knapp-Kantine Wrapper • 2026 by Kaufis-Kitchen") und Footer-Höhe für mehr Platzierung optimiert.
|
||||||
|
|
||||||
## v1.6.2 (2026-03-05)
|
## v1.6.2 (2026-03-05)
|
||||||
- ✨ **Feature**: Wochentags-Header (Montag, Dienstag etc.) scrollen nun als "Sticky Header" mit und bleiben am oberen Bildschirmrand haften.
|
- ✨ **Feature**: Wochentags-Header (Montag, Dienstag etc.) scrollen nun als "Sticky Header" mit und bleiben am oberen Bildschirmrand haften.
|
||||||
- Das Layout clippt scrollende Speisen ordentlich darunter weg.
|
- Das Layout clippt scrollende Speisen ordentlich darunter weg.
|
||||||
|
|||||||
9
dist/bookmarklet-payload.js
vendored
9
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
47
dist/install.html
vendored
47
dist/install.html
vendored
File diff suppressed because one or more lines are too long
189
dist/kantine-standalone.html
vendored
189
dist/kantine-standalone.html
vendored
File diff suppressed because one or more lines are too long
BIN
favicon.png
BIN
favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.6 KiB |
29
favicon.svg
29
favicon.svg
@@ -1,29 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
||||||
<!-- Fork (left, with gap to triangle) -->
|
|
||||||
<g transform="translate(2, 10)">
|
|
||||||
<!-- Tines -->
|
|
||||||
<rect x="1" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<rect x="4.6" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<rect x="8.2" y="0" width="1.8" height="16" rx="0.9" fill="#333"/>
|
|
||||||
<!-- Connector -->
|
|
||||||
<rect x="1" y="14" width="9" height="3.5" rx="1.5" fill="#333"/>
|
|
||||||
<!-- Handle -->
|
|
||||||
<rect x="3.5" y="16.5" width="4" height="24" rx="2" fill="#333"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Triangle (center, equilateral aspect ratio ~1:0.866) -->
|
|
||||||
<!-- Equilateral: base=28, height=24.25 => keeps proper ratio -->
|
|
||||||
<polygon points="32,8 47,48 17,48" fill="none" stroke="#333" stroke-width="4" stroke-linejoin="round"/>
|
|
||||||
|
|
||||||
<!-- Knife (right, with gap to triangle) -->
|
|
||||||
<g transform="translate(50, 10)">
|
|
||||||
<!-- Blade (slight curve) -->
|
|
||||||
<path d="M3,0 C3,0 3,0 3,0 L3,17 L10,14 C10,6 7,0 3,0 Z" fill="#333"/>
|
|
||||||
<!-- Spine -->
|
|
||||||
<rect x="1.5" y="0" width="2" height="18" rx="1" fill="#333"/>
|
|
||||||
<!-- Bolster -->
|
|
||||||
<rect x="1.5" y="16.5" width="8.5" height="3.5" rx="1.2" fill="#333"/>
|
|
||||||
<!-- Handle -->
|
|
||||||
<rect x="3.5" y="19" width="4" height="22" rx="2" fill="#333"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
148
kantine.js
148
kantine.js
@@ -80,7 +80,7 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
<img src="{{FAVICON_DATA_URI}}" alt="Logo" class="logo-img" style="height: 2em; width: 2em; object-fit: contain;">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">{{VERSION}}</small></h1>
|
<h1>Kantinen Übersicht <small class="version-tag" style="font-size: 0.6em; opacity: 0.7; font-weight: 400; cursor: pointer;" title="Klick für Versionsmenü">{{VERSION}}</small></h1>
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
<div id="last-updated-subtitle" class="subtitle"></div>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<p>Bessa Knapp-Kantine Wrapper • <span id="current-year">${new Date().getFullYear()}</span></p>
|
<p>Jetzt Bessa Einfach! • Knapp-Kantine Wrapper • <span id="current-year">${new Date().getFullYear()}</span> by Kaufi 😃👍 mit Hilfe von KI 🤖</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -1309,6 +1309,25 @@
|
|||||||
updateNextWeekBadge();
|
updateNextWeekBadge();
|
||||||
updateAlarmBell();
|
updateAlarmBell();
|
||||||
if (cachedTs) updateLastUpdatedTime(cachedTs);
|
if (cachedTs) updateLastUpdatedTime(cachedTs);
|
||||||
|
|
||||||
|
// --- TEMP DEBUG LOGGER ---
|
||||||
|
try {
|
||||||
|
const uniqueMenus = new Set();
|
||||||
|
allWeeks.forEach(w => {
|
||||||
|
(w.days || []).forEach(d => {
|
||||||
|
(d.items || []).forEach(item => {
|
||||||
|
let text = (item.description || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (text && text.includes(' / ')) {
|
||||||
|
uniqueMenus.add(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const res = Array.from(uniqueMenus).join('\n\n');
|
||||||
|
console.log("=== GEFUNDENE MENÜ-TEXTE (" + uniqueMenus.size + ") ===");
|
||||||
|
console.log(res);
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
console.log('Loaded menu from cache');
|
console.log('Loaded menu from cache');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2361,29 +2380,35 @@
|
|||||||
// === Language Filter (FR-100) ===
|
// === Language Filter (FR-100) ===
|
||||||
// DE stems for fallback language detection
|
// DE stems for fallback language detection
|
||||||
const DE_STEMS = [
|
const DE_STEMS = [
|
||||||
'mit', 'und', 'oder', 'für', 'vom', 'zum', 'zur', 'gebraten', 'kartoffel', 'gemüse', 'suppe',
|
'apfel', 'aubergine', 'auflauf', 'beere', 'blumenkohl', 'bohne', 'braten', 'brokkoli', 'brot', 'brust',
|
||||||
'kuchen', 'schwein', 'rind', 'hähnchen', 'huhn', 'fisch', 'nudel', 'soße', 'sosse', 'wurst',
|
'brötchen', 'butter', 'chili', 'dessert', 'dip', 'eier', 'eintopf', 'eis', 'erbse', 'erdbeer',
|
||||||
'kürbis', 'braten', 'sahne', 'apfel', 'käse', 'fleisch', 'pilz', 'kirsch', 'joghurt', 'spätzle',
|
'essig', 'filet', 'fisch', 'fisole', 'fleckerl', 'fleisch', 'flügel', 'frucht', 'für', 'gebraten',
|
||||||
'knödel', 'kraut', 'schnitzel', 'püree', 'rahm', 'erdbeer', 'schoko', 'vanille', 'tomate',
|
'gemüse', 'gewürz', 'gratin', 'grieß', 'gulasch', 'gurke', 'himbeer', 'honig', 'huhn', 'hähnchen',
|
||||||
'gurke', 'salat', 'zwiebel', 'paprika', 'reis', 'bohne', 'erbse', 'karotte', 'möhre', 'lauch',
|
'jambalaya', 'joghurt', 'karotte', 'kartoffel', 'keule', 'kirsch', 'knacker', 'knoblauch', 'knödel', 'kompott',
|
||||||
'knoblauch', 'chili', 'gewürz', 'kräuter', 'pfeffer', 'salz', 'butter', 'milch', 'eier',
|
'kraut', 'kräuter', 'kuchen', 'käse', 'kürbis', 'lauch', 'mandel', 'milch', 'mild', 'mit',
|
||||||
'pfanne', 'auflauf', 'gratin', 'ragout', 'gulasch', 'eintopf', 'filet', 'steak', 'brust',
|
'mohn', 'most', 'möhre', 'natur', 'nockerl', 'nudel', 'nuss', 'nuß', 'obst', 'oder',
|
||||||
'salami', 'schinken', 'speck', 'brokkoli', 'blumenkohl', 'zucchini', 'aubergine',
|
'olive', 'paprika', 'pfanne', 'pfannkuchen', 'pfeffer', 'pikant', 'pilz', 'plunder', 'püree', 'ragout',
|
||||||
'spinat', 'spargel', 'olive', 'mandel', 'nuss', 'honig', 'senf', 'essig', 'öl', 'brot',
|
'rahm', 'reis', 'rind', 'sahne', 'salami', 'salat', 'salz', 'sauer', 'scharf', 'schinken',
|
||||||
'brötchen', 'pfannkuchen', 'eis', 'torte', 'dessert', 'kompott', 'obst', 'frucht', 'beere',
|
'schnitte', 'schnitzel', 'schoko', 'schupf', 'schwein', 'sellerie', 'senf', 'sosse', 'soße', 'spargel',
|
||||||
'plunder', 'dip', 'tofu', 'jambalaya'
|
'spätzle', 'speck', 'spieß', 'spinat', 'steak', 'suppe', 'süß', 'tofu', 'tomate', 'topfen',
|
||||||
|
'torte', 'trüffel', 'und', 'vanille', 'vogerl', 'vom', 'wien', 'wurst', 'zucchini', 'zum',
|
||||||
|
'zur', 'zwiebel', 'öl'
|
||||||
];
|
];
|
||||||
|
|
||||||
const EN_STEMS = [
|
const EN_STEMS = [
|
||||||
'with', 'and', 'or', 'for', 'from', 'to', 'fried', 'potato', 'vegetable', 'soup', 'cake',
|
'almond', 'and', 'apple', 'asparagus', 'bacon', 'baked', 'ball', 'bean', 'beef', 'berry',
|
||||||
'pork', 'beef', 'chicken', 'fish', 'noodle', 'sauce', 'sausage', 'pumpkin', 'roast',
|
'bread', 'breast', 'broccoli', 'bun', 'butter', 'cabbage', 'cake', 'caper', 'carrot', 'casserole',
|
||||||
'cream', 'apple', 'cheese', 'meat', 'mushroom', 'cherry', 'yogurt', 'wedge', 'sweet',
|
'cauliflower', 'celery', 'cheese', 'cherry', 'chicken', 'chili', 'choco', 'chocolate', 'cider', 'cilantro',
|
||||||
'sour', 'dumpling', 'cabbage', 'mash', 'strawberr', 'choco', 'vanilla', 'tomat', 'cucumber',
|
'coffee', 'compote', 'cream', 'cucumber', 'curd', 'danish', 'dessert', 'dip', 'dumpling', 'egg',
|
||||||
'salad', 'onion', 'pepper', 'rice', 'bean', 'pea', 'carrot', 'leek', 'garlic', 'chili',
|
'eggplant', 'filet', 'fish', 'for', 'fried', 'from', 'fruit', 'garlic', 'goulash', 'gratin',
|
||||||
'spice', 'herb', 'salt', 'butter', 'milk', 'egg', 'pan', 'casserole', 'gratin', 'ragout',
|
'ham', 'herb', 'honey', 'hot', 'ice', 'jambalaya', 'leek', 'leg', 'mash', 'meat',
|
||||||
'goulash', 'stew', 'filet', 'steak', 'breast', 'salami', 'ham', 'bacon', 'broccoli',
|
'mexican', 'mild', 'milk', 'mint', 'mushroom', 'mustard', 'noodle', 'nut', 'oat', 'oil',
|
||||||
'cauliflower', 'zucchini', 'eggplant', 'spinach', 'asparagus', 'olive', 'almond', 'nut',
|
'olive', 'onion', 'or', 'oven', 'pan', 'pancake', 'pea', 'pepper', 'plain', 'plate',
|
||||||
'honey', 'mustard', 'vinegar', 'oil', 'bread', 'bun', 'pancake', 'ice', 'tart', 'dessert',
|
'poppy', 'pork', 'potato', 'pumpkin', 'radish', 'ragout', 'raspberry', 'rice', 'roast', 'roll',
|
||||||
'compote', 'fruit', 'berry', 'dip', 'danish', 'tofu', 'jambalaya'
|
'salad', 'salami', 'salt', 'sauce', 'sausage', 'shrimp', 'skewer', 'slice', 'soup', 'sour',
|
||||||
|
'spice', 'spicy', 'spinach', 'steak', 'stew', 'strawberr', 'strawberry', 'strudel', 'sweet', 'tart',
|
||||||
|
'thyme', 'to', 'tofu', 'tomat', 'tomato', 'truffle', 'trukey', 'turkey', 'vanilla', 'vegan',
|
||||||
|
'vegetable', 'vinegar', 'wedge', 'wing', 'with', 'wok', 'yogurt', 'zucchini'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2397,7 +2422,11 @@
|
|||||||
if (!text) return { de: '', en: '', raw: '' };
|
if (!text) return { de: '', en: '', raw: '' };
|
||||||
|
|
||||||
const raw = text;
|
const raw = text;
|
||||||
const formattedRaw = '• ' + text.replace(/\(([A-Z ]+)\)\s*(?=\S)/g, '($1)\n• ');
|
// Formatting: add • for new lines, using the forgiving regex
|
||||||
|
let formattedRaw = text.replace(/(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*(?=\S)/g, '($1)\n• ');
|
||||||
|
if (!formattedRaw.startsWith('• ')) {
|
||||||
|
formattedRaw = '• ' + formattedRaw;
|
||||||
|
}
|
||||||
|
|
||||||
// Utility to compute DE/EN score for a subset of words
|
// Utility to compute DE/EN score for a subset of words
|
||||||
function scoreBlock(wordArray) {
|
function scoreBlock(wordArray) {
|
||||||
@@ -2427,7 +2456,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic sliding window to split a fragment containing "EN DE"
|
// Heuristic sliding window to split a fragment containing "EN DE"
|
||||||
// E.g., "Bratwurst with pumpkin Kirschjoghurt" => enPart: "Bratwurst with pumpkin", dePart: "Kirschjoghurt"
|
|
||||||
function heuristicSplitEnDe(fragment) {
|
function heuristicSplitEnDe(fragment) {
|
||||||
const words = fragment.trim().split(/\s+/);
|
const words = fragment.trim().split(/\s+/);
|
||||||
if (words.length < 2) return { enPart: fragment, nextDe: '' };
|
if (words.length < 2) return { enPart: fragment, nextDe: '' };
|
||||||
@@ -2442,22 +2470,20 @@
|
|||||||
const leftScore = scoreBlock(left);
|
const leftScore = scoreBlock(left);
|
||||||
const rightScore = scoreBlock(right);
|
const rightScore = scoreBlock(right);
|
||||||
|
|
||||||
// left should be EN, right should be DE
|
|
||||||
// Metric = (EN votes in left - DE votes in left) + (DE votes in right - EN votes in right)
|
|
||||||
const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en);
|
|
||||||
|
|
||||||
// Extra penalty if the split puts a low-case word as the first word of the right (DE) part
|
|
||||||
// because a new German sentence usually starts with a capital noun.
|
|
||||||
const rightFirstWord = right[0];
|
const rightFirstWord = right[0];
|
||||||
let capitalBonus = 0;
|
let capitalBonus = 0;
|
||||||
|
// Nouns are capitalized in German
|
||||||
if (/^[A-ZÄÖÜ]/.test(rightFirstWord)) {
|
if (/^[A-ZÄÖÜ]/.test(rightFirstWord)) {
|
||||||
capitalBonus = 2.0;
|
capitalBonus = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalScore = score + capitalBonus;
|
const score = (leftScore.en - leftScore.de) + (rightScore.de - rightScore.en) + capitalBonus;
|
||||||
|
|
||||||
if (finalScore > maxScore) {
|
// Strict condition! The assumed German part must actually look German
|
||||||
maxScore = finalScore;
|
const rightLooksGerman = (rightScore.de + capitalBonus) > rightScore.en;
|
||||||
|
|
||||||
|
if (rightLooksGerman && score > maxScore) {
|
||||||
|
maxScore = score;
|
||||||
bestK = k;
|
bestK = k;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2471,50 +2497,34 @@
|
|||||||
return { enPart: fragment, nextDe: '' };
|
return { enPart: fragment, nextDe: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if text contains the bilingual separator ' / '
|
// NEW LOGIC: We no longer split by slash if the slash is part of a missing-parenthesis allergen like /ACGL)
|
||||||
if (!text.includes(' / ')) {
|
const parts = text.split(/\s*\/\s*(?![A-Z,]+\))/);
|
||||||
// Fallback: detect language via keyword scoring
|
|
||||||
const words = text.toLowerCase().split(/\s+/);
|
|
||||||
const score = scoreBlock(words);
|
|
||||||
|
|
||||||
// No split possible – return full text for detected language, empty for other
|
|
||||||
if (score.en > score.de) {
|
|
||||||
return { de: '', en: formattedRaw, raw: formattedRaw };
|
|
||||||
}
|
|
||||||
return { de: formattedRaw, en: '', raw: formattedRaw };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split by ' / ' – produces alternating DE/EN fragments
|
|
||||||
const parts = text.split(' / ');
|
|
||||||
// Sanity check: max 3 courses means max 3 slashes → max 4 parts
|
// Sanity check: max 3 courses means max 3 slashes → max 4 parts
|
||||||
if (parts.length > 4) {
|
if (parts.length > 4) {
|
||||||
// Too many slashes – possibly not bilingual, return as-is
|
|
||||||
return { de: formattedRaw, en: '', raw: formattedRaw };
|
return { de: formattedRaw, en: '', raw: formattedRaw };
|
||||||
}
|
}
|
||||||
|
|
||||||
const deParts = [];
|
const deParts = [];
|
||||||
const enParts = [];
|
const enParts = [];
|
||||||
|
|
||||||
// First fragment is always DE (course 1)
|
// Part 0 is ALWAYS German (beginning of the menu item)
|
||||||
deParts.push(parts[0].trim());
|
deParts.push(parts[0].trim());
|
||||||
|
|
||||||
// Process remaining fragments: each contains "EN(ALLERGENS) next_DE"
|
// Matches e.g., "(GLM)" OR "/GLM)" OR " GLM)" with trailing spaces
|
||||||
// Allergen pattern: (LETTERS_AND_SPACES) at the boundary
|
const allergenRegex = /(?:\(|(?:\/|\s|^))([A-Z,]+)\)\s*/;
|
||||||
const allergenRegex = /\(([A-Z ]+)\)\s*/;
|
|
||||||
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
const fragment = parts[i].trim();
|
const fragment = parts[i].trim();
|
||||||
const match = fragment.match(allergenRegex);
|
const match = fragment.match(allergenRegex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// Split: everything before allergen + allergen = EN, after = next DE
|
|
||||||
const allergenEnd = match.index + match[0].length;
|
const allergenEnd = match.index + match[0].length;
|
||||||
const enPart = fragment.substring(0, match.index).trim();
|
const enPart = fragment.substring(0, match.index).trim();
|
||||||
const allergenCode = match[1];
|
const allergenCode = match[1];
|
||||||
const nextDe = fragment.substring(allergenEnd).trim();
|
const nextDe = fragment.substring(allergenEnd).trim();
|
||||||
|
|
||||||
enParts.push(enPart + '(' + allergenCode + ')');
|
enParts.push(enPart + '(' + allergenCode + ')');
|
||||||
// Also append allergen to the last DE part
|
|
||||||
if (deParts.length > 0) {
|
if (deParts.length > 0) {
|
||||||
deParts[deParts.length - 1] = deParts[deParts.length - 1] + '(' + allergenCode + ')';
|
deParts[deParts.length - 1] = deParts[deParts.length - 1] + '(' + allergenCode + ')';
|
||||||
}
|
}
|
||||||
@@ -2523,9 +2533,7 @@
|
|||||||
deParts.push(nextDe);
|
deParts.push(nextDe);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No allergen code found!
|
// No allergen code found! Need to heuristically split "EN DE"
|
||||||
// If it's not the last part (or even if it is, but we highly suspect merged languages),
|
|
||||||
// we use the heuristic to find the hidden split-point.
|
|
||||||
const split = heuristicSplitEnDe(fragment);
|
const split = heuristicSplitEnDe(fragment);
|
||||||
enParts.push(split.enPart);
|
enParts.push(split.enPart);
|
||||||
if (split.nextDe) {
|
if (split.nextDe) {
|
||||||
@@ -2534,9 +2542,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIX FOR SINGLE-LANGUAGE COURSES OR MISSING EN
|
||||||
|
if (parts.length === 1 && enParts.length === 0) {
|
||||||
|
enParts.push(deParts[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirror untranslated DE courses to EN (e.g. Dessert)
|
||||||
|
if (deParts.length > enParts.length) {
|
||||||
|
for (let i = enParts.length; i < deParts.length; i++) {
|
||||||
|
enParts.push(deParts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let deJoined = deParts.join('\n• ');
|
||||||
|
if (deParts.length > 0 && !deJoined.startsWith('• ')) deJoined = '• ' + deJoined;
|
||||||
|
|
||||||
|
let enJoined = enParts.join('\n• ');
|
||||||
|
if (enParts.length > 0 && !enJoined.startsWith('• ')) enJoined = '• ' + enJoined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
de: deParts.map(p => '• ' + p).join('\n'),
|
de: deJoined,
|
||||||
en: enParts.map(p => '• ' + p).join('\n'),
|
en: enJoined,
|
||||||
raw: formattedRaw
|
raw: formattedRaw
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
31
style.css
31
style.css
@@ -426,7 +426,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 2rem 0 0 0;
|
padding: 0 0 0 0;
|
||||||
/* Only top padding, no horizontal so child fills width */
|
/* Only top padding, no horizontal so child fills width */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -875,42 +875,31 @@ body {
|
|||||||
filter: grayscale(0.4);
|
filter: grayscale(0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhancements for ordered items */
|
/* Past ordered items get no special frame or shadow, but remain visually distinct by staying fully opaque (via the :not(.ordered) selector above) */
|
||||||
.menu-card.past-day .menu-item.ordered {
|
|
||||||
/* No opacity/filter here - fully visible */
|
|
||||||
background: var(--bg-card);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
border: 1px solid #8b5cf6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 0 -1rem 1.5rem -1rem;
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-item.today-ordered {
|
.menu-item.today-ordered {
|
||||||
border: 2px solid #8b5cf6;
|
border: 2px solid #8b5cf6;
|
||||||
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
box-shadow: 0 0 30px rgba(139, 92, 246, 0.6);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0 -1rem 1.5rem -1rem;
|
margin: 0 -1rem 1.5rem -1rem;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
animation: pulse-glow 3s infinite;
|
animation: pulse-glow-strong 3s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
@keyframes pulse-glow-strong {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3);
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 25px rgba(139, 92, 246, 0.6);
|
box-shadow: 0 0 40px rgba(139, 92, 246, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 15px rgba(139, 92, 246, 0.3);
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1084,9 +1073,9 @@ body {
|
|||||||
.app-footer {
|
.app-footer {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1rem 2rem;
|
padding: 0.4rem 2rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.875rem;
|
font-size: 0.8rem;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,20 +41,32 @@ const sandbox = {
|
|||||||
fetch: async (url) => {
|
fetch: async (url) => {
|
||||||
// Mock Version Check
|
// Mock Version Check
|
||||||
if (url.includes('version.txt')) {
|
if (url.includes('version.txt')) {
|
||||||
return { ok: true, text: async () => 'v9.9.9' }; // Simulate new version
|
return { ok: true, text: async () => 'v9.9.9', json: async () => ({}) };
|
||||||
}
|
}
|
||||||
// Mock Changelog
|
// Mock Changelog
|
||||||
if (url.includes('changelog.md')) {
|
if (url.includes('changelog.md')) {
|
||||||
return { ok: true, text: async () => '## v9.9.9\n- Feature: Cool Stuff' };
|
return { ok: true, text: async () => '## v9.9.9\n- Feature: Cool Stuff', json: async () => ({}) };
|
||||||
}
|
}
|
||||||
return { ok: false }; // Fail others to prevent huge cascades
|
// Mock GitHub Tags API
|
||||||
|
if (url.includes('api.github.com/') || url.includes('/tags')) {
|
||||||
|
return { ok: true, json: async () => [{ name: 'v9.9.9' }] };
|
||||||
|
}
|
||||||
|
// Mock Menu API
|
||||||
|
if (url.includes('/food-menu/menu/')) {
|
||||||
|
return { ok: true, json: async () => ({ dates: [], menu: {} }) };
|
||||||
|
}
|
||||||
|
// Mock Orders API
|
||||||
|
if (url.includes('/user/orders')) {
|
||||||
|
return { ok: true, json: async () => ({ results: [], count: 0 }) };
|
||||||
|
}
|
||||||
|
return { ok: false, status: 404, text: async () => '', json: async () => ({}) };
|
||||||
},
|
},
|
||||||
document: {
|
document: {
|
||||||
body: createMockElement('body'),
|
body: createMockElement('body'),
|
||||||
head: createMockElement('head'),
|
head: createMockElement('head'),
|
||||||
createElement: (tag) => createMockElement(tag),
|
createElement: (tag) => createMockElement(tag),
|
||||||
querySelector: (sel) => {
|
querySelector: (sel) => {
|
||||||
if (sel === '.material-icons-round.logo-icon') {
|
if (sel === '.logo-img' || sel === '.material-icons-round.logo-icon') {
|
||||||
const el = createMockElement('last-updated-icon-mock');
|
const el = createMockElement('last-updated-icon-mock');
|
||||||
// Mock legacy prop for specific test check if needed,
|
// Mock legacy prop for specific test check if needed,
|
||||||
// but our generic mock handles replaceWith hook
|
// but our generic mock handles replaceWith hook
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.6.2
|
v1.6.8
|
||||||
|
|||||||
Reference in New Issue
Block a user