fix(ui): v1.2.1 – highlights integration, mock data, CSS polish

This commit is contained in:
2026-02-16 21:33:18 +01:00
parent bab54fdf2d
commit 1040828d7f
11 changed files with 563 additions and 44 deletions

View File

@@ -475,7 +475,6 @@ body {
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid var(--border-color);
/* Changed from --border */
}
.modal-header h2 {
@@ -483,6 +482,10 @@ body {
font-size: 1.25rem;
}
.modal-body {
padding: 20px;
}
#login-form {
padding: 20px;
}
@@ -1247,14 +1250,31 @@ body {
}
}
/* 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);
/* Smart Highlights (Blue Glow - matches today-ordered/flagged pattern) */
.menu-item.highlight-glow {
border: 2px solid rgba(59, 130, 246, 0.7);
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
border-radius: 8px;
padding: 1rem;
margin: 0 -1rem 1.5rem -1rem;
background: var(--bg-card);
position: relative;
z-index: 1;
z-index: 5;
animation: blue-pulse 3s infinite;
}
@keyframes blue-pulse {
0% {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
}
50% {
box-shadow: 0 0 25px rgba(59, 130, 246, 0.6);
}
100% {
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
}
}
/* Nav Badge with Count */
@@ -1282,23 +1302,32 @@ body {
min-height: 50px;
}
/* Tag badges styled consistently with .badge (verfügbar/ausverkauft) */
.tag-badge {
display: inline-flex;
align-items: center;
background: rgba(59, 130, 246, 0.15);
justify-content: center;
height: 24px;
font-size: 0.75rem;
padding: 0 10px;
border-radius: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: normal;
white-space: nowrap;
background-color: rgba(59, 130, 246, 0.1);
color: #3b82f6;
padding: 4px 10px;
border-radius: 99px;
font-size: 0.85rem;
font-weight: 500;
border: 1px solid rgba(59, 130, 246, 0.2);
gap: 4px;
}
.tag-remove {
margin-left: 6px;
cursor: pointer;
opacity: 0.7;
font-size: 1.1em;
line-height: 1;
transition: all 0.2s;
}
.tag-remove:hover {
@@ -1318,6 +1347,30 @@ body {
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
font-family: inherit;
}
/* Add tag button - styled like .btn-order with nav-btn.active color */
#btn-add-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
background: var(--accent-color);
color: white;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
white-space: nowrap;
}
#btn-add-tag:hover {
filter: brightness(1.15);
transform: translateY(-1px);
}
/* Update Banner Enhanced */
@@ -1361,6 +1414,191 @@ body {
</head>
<body>
<script>
/**
* Mock data for standalone HTML testing.
* Intercepts fetch() calls to api.bessa.app and returns realistic dummy data.
* Injected BEFORE kantine.js in standalone builds only.
*/
(function () {
'use strict';
// Generate dates for this week and next week (Mon-Fri)
function getWeekDates(weekOffset) {
const dates = [];
const now = new Date();
const dayOfWeek = now.getDay(); // 0=Sun, 1=Mon
const monday = new Date(now);
monday.setDate(now.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1) + (weekOffset * 7));
for (let i = 0; i < 5; i++) {
const d = new Date(monday);
d.setDate(monday.getDate() + i);
dates.push(d.toISOString().split('T')[0]);
}
return dates;
}
const thisWeekDates = getWeekDates(0);
const nextWeekDates = getWeekDates(1);
const allDates = [...thisWeekDates, ...nextWeekDates];
// Realistic German canteen menu items per day
const menuPool = [
[
{ id: 101, name: 'Wiener Schnitzel mit Kartoffelsalat', description: 'Paniertes Schweineschnitzel mit hausgemachtem Kartoffelsalat', price: '6.90', available_amount: '15', amount_tracking: true },
{ id: 102, name: 'Gemüse-Curry mit Basmatireis', description: 'Veganes Curry mit saisonalem Gemüse und Kokosmilch', price: '5.50', available_amount: '0', amount_tracking: true },
{ id: 103, name: 'Rindergulasch mit Spätzle', description: 'Geschmortes Rindfleisch in Paprikasauce mit Eierspätzle', price: '7.20', available_amount: '8', amount_tracking: true },
{ id: 104, name: 'Tagessuppe: Tomatencremesuppe', description: 'Cremige Tomatensuppe mit Croutons', price: '3.20', available_amount: '0', amount_tracking: false },
],
[
{ id: 201, name: 'Hähnchenbrust mit Pilzrahmsauce', description: 'Gebratene Hähnchenbrust mit Champignon-Rahmsauce und Reis', price: '6.50', available_amount: '12', amount_tracking: true },
{ id: 202, name: 'Vegetarische Lasagne', description: 'Lasagne mit Spinat, Ricotta und Tomatensauce', price: '5.80', available_amount: '10', amount_tracking: true },
{ id: 203, name: 'Bratwurst mit Sauerkraut', description: 'Thüringer Bratwurst mit Sauerkraut und Kartoffelpüree', price: '5.90', available_amount: '0', amount_tracking: true },
{ id: 204, name: 'Caesar Salad mit Hähnchen', description: 'Römersalat mit gegrilltem Hähnchen, Parmesan und Croutons', price: '6.10', available_amount: '0', amount_tracking: false },
],
[
{ id: 301, name: 'Spaghetti Bolognese', description: 'Klassische Bolognese mit frischen Spaghetti', price: '5.20', available_amount: '20', amount_tracking: true },
{ id: 302, name: 'Gebratener Lachs mit Dillsauce', description: 'Lachsfilet auf Blattspinat mit Senf-Dill-Sauce', price: '8.50', available_amount: '5', amount_tracking: true },
{ id: 303, name: 'Kartoffelgratin mit Salat', description: 'Überbackene Kartoffeln mit Sahne und Käse, dazu gemischter Salat', price: '5.00', available_amount: '0', amount_tracking: false },
{ id: 304, name: 'Chili con Carne', description: 'Pikantes Chili mit Hackfleisch, Bohnen und Reis', price: '5.80', available_amount: '9', amount_tracking: true },
],
[
{ id: 401, name: 'Schweinebraten mit Knödel', description: 'Bayerischer Schweinebraten mit Semmelknödel und Bratensauce', price: '7.00', available_amount: '7', amount_tracking: true },
{ id: 402, name: 'Falafel-Bowl mit Hummus', description: 'Knusprige Falafel mit Hummus, Tabouleh und Fladenbrot', price: '5.90', available_amount: '0', amount_tracking: false },
{ id: 403, name: 'Putengeschnetzeltes mit Nudeln', description: 'Putenstreifen in Champignon-Sahnesauce mit Bandnudeln', price: '6.30', available_amount: '11', amount_tracking: true },
{ id: 404, name: 'Tagessuppe: Erbsensuppe', description: 'Deftige Erbsensuppe mit Wiener Würstchen', price: '3.50', available_amount: '0', amount_tracking: false },
],
[
{ id: 501, name: 'Backfisch mit Remoulade', description: 'Paniertes Seelachsfilet mit Remouladensauce und Bratkartoffeln', price: '6.80', available_amount: '6', amount_tracking: true },
{ id: 502, name: 'Käsespätzle mit Röstzwiebeln', description: 'Allgäuer Käsespätzle mit karamellisierten Zwiebeln und Salat', price: '5.50', available_amount: '14', amount_tracking: true },
{ id: 503, name: 'Schnitzel Wiener Art mit Pommes', description: 'Paniertes Hähnchenschnitzel mit knusprigen Pommes Frites', price: '6.20', available_amount: '0', amount_tracking: true },
{ id: 504, name: 'Griechischer Bauernsalat', description: 'Frischer Salat mit Feta, Oliven, Gurke und Tomaten', price: '5.30', available_amount: '0', amount_tracking: false },
],
];
// Build mock responses for each date
const dateResponses = {};
allDates.forEach((date, i) => {
const menuIndex = i % menuPool.length;
dateResponses[date] = {
results: [{
id: 1,
name: 'Mittagsmenü',
items: menuPool[menuIndex].map(item => ({
...item,
// Ensure unique IDs per date
id: item.id + (i * 1000)
}))
}]
};
});
// Mock some orders for today (to show "Bestellt" badges)
const todayStr = new Date().toISOString().split('T')[0];
const todayMenu = dateResponses[todayStr];
const mockOrders = [];
let nextOrderId = 9001;
if (todayMenu) {
const firstItem = todayMenu.results[0].items[0];
mockOrders.push({
id: nextOrderId++,
article: firstItem.id,
article_name: firstItem.name,
date: todayStr,
venue: 591,
status: 'confirmed',
created: new Date().toISOString()
});
}
// Pre-seed a mock auth session so flag/order buttons render
sessionStorage.setItem('kantine_authToken', 'mock-token-for-testing');
sessionStorage.setItem('kantine_currentUser', '12345');
sessionStorage.setItem('kantine_firstName', 'Test');
sessionStorage.setItem('kantine_lastName', 'User');
// Intercept fetch
const originalFetch = window.fetch;
window.fetch = function (url, options) {
const urlStr = typeof url === 'string' ? url : url.toString();
// Menu dates endpoint
if (urlStr.includes('/menu/dates/')) {
console.log('[MOCK] Returning mock dates data');
return Promise.resolve(new Response(JSON.stringify({
results: allDates.map(date => ({ date, orders: [] }))
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
}
// Menu detail for a specific date
const dateMatch = urlStr.match(/\/menu\/\d+\/(\d{4}-\d{2}-\d{2})\//);
if (dateMatch) {
const date = dateMatch[1];
const data = dateResponses[date] || { results: [] };
console.log(`[MOCK] Returning mock menu for ${date}`);
return Promise.resolve(new Response(JSON.stringify(data), {
status: 200, headers: { 'Content-Type': 'application/json' }
}));
}
// Orders endpoint
if (urlStr.includes('/user/orders/') && (!options || options.method === 'GET' || !options.method)) {
console.log('[MOCK] Returning mock orders');
return Promise.resolve(new Response(JSON.stringify({
results: mockOrders
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
}
// Auth user endpoint
if (urlStr.includes('/auth/user/')) {
console.log('[MOCK] Returning mock user');
return Promise.resolve(new Response(JSON.stringify({
pk: 12345,
username: 'testuser',
email: 'test@example.com',
first_name: 'Test',
last_name: 'User'
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
}
// Order create (POST to /user/orders/)
if (urlStr.includes('/user/orders/') && options && options.method === 'POST') {
const body = JSON.parse(options.body || '{}');
const newOrder = {
id: nextOrderId++,
article: body.article,
article_name: 'Mock Order',
date: body.date,
venue: 591,
status: 'confirmed',
created: new Date().toISOString()
};
mockOrders.push(newOrder);
console.log('[MOCK] Created order:', newOrder);
return Promise.resolve(new Response(JSON.stringify(newOrder), {
status: 201, headers: { 'Content-Type': 'application/json' }
}));
}
// Order cancel (POST to /user/orders/{id}/cancel/)
const cancelMatch = urlStr.match(/\/user\/orders\/(\d+)\/cancel\//);
if (cancelMatch) {
const orderId = parseInt(cancelMatch[1]);
const idx = mockOrders.findIndex(o => o.id === orderId);
if (idx >= 0) mockOrders.splice(idx, 1);
console.log('[MOCK] Cancelled order:', orderId);
return Promise.resolve(new Response('{}', {
status: 200, headers: { 'Content-Type': 'application/json' }
}));
}
// Fallback to real fetch for other URLs (fonts, etc.)
return originalFetch.apply(this, arguments);
};
console.log('[MOCK] 🧪 Mock data active using dummy canteen menus for UI testing');
})();
/**
* Kantine Wrapper Client-Only Bookmarklet
* Replaces Bessa page content with enhanced weekly menu view.
@@ -1429,7 +1667,7 @@ body {
<div class="brand">
<span class="material-icons-round logo-icon">restaurant_menu</span>
<div class="header-left">
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.0</small></h1>
<h1>Kantinen Übersicht <small style="font-size: 0.6em; opacity: 0.7; font-weight: 400;">v1.2.1</small></h1>
<div id="last-updated-subtitle" class="subtitle"></div>
</div>
</div>
@@ -2673,6 +2911,11 @@ body {
itemEl.classList.add(item.available ? 'flagged-available' : 'flagged-sold-out');
}
// Highlight matching menu items based on user tags
if (checkHighlight(item.name) || checkHighlight(item.description)) {
itemEl.classList.add('highlight-glow');
}
// Action buttons
let orderButton = '';
let cancelButton = '';
@@ -2762,7 +3005,7 @@ body {
// === Version Check ===
async function checkForUpdates() {
const CurrentVersion = 'v1.2.0';
const CurrentVersion = 'v1.2.1';
const VersionUrl = 'https://raw.githubusercontent.com/TauNeutrino/kantine-overview/main/version.txt';
const InstallerUrl = 'https://htmlpreview.github.io/?https://github.com/TauNeutrino/kantine-overview/blob/main/dist/install.html';