feat: Text 2 als feste Inhaltsangabe und Langtext 1-4 als aufklappbare Details (Hover/Touch)
All checks were successful
Build and Deploy / build (push) Successful in 54s

This commit is contained in:
DanielS
2026-09-22 23:36:12 +02:00
parent 7f1e17db2b
commit 1b0726deef
4 changed files with 165 additions and 24 deletions

14
db.js
View File

@@ -3,7 +3,6 @@ const path = require('path');
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
// Verzeichnis für Persistenz sicherstellen (/data im Docker Container oder local dir)
const dataDir = process.env.DATA_DIR || (fs.existsSync('/data') ? '/data' : __dirname);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
@@ -12,7 +11,6 @@ if (!fs.existsSync(dataDir)) {
const dbPath = path.join(dataDir, 'data.db');
const db = new Database(dbPath);
// Standard Journal Mode
db.pragma('journal_mode = WAL');
// Tabellen anlegen
@@ -32,6 +30,10 @@ db.exec(`
id TEXT PRIMARY KEY,
text1 TEXT NOT NULL,
text2 TEXT,
langtext1 TEXT,
langtext2 TEXT,
langtext3 TEXT,
langtext4 TEXT,
group_id TEXT,
preis1 REAL DEFAULT 0,
unit TEXT,
@@ -49,6 +51,14 @@ db.exec(`
);
`);
// Migration für bestehende DBs falls Spalten fehlen
const tableInfo = db.prepare("PRAGMA table_info(products)").all();
const cols = tableInfo.map(c => c.name);
if (!cols.includes('langtext1')) db.exec("ALTER TABLE products ADD COLUMN langtext1 TEXT;");
if (!cols.includes('langtext2')) db.exec("ALTER TABLE products ADD COLUMN langtext2 TEXT;");
if (!cols.includes('langtext3')) db.exec("ALTER TABLE products ADD COLUMN langtext3 TEXT;");
if (!cols.includes('langtext4')) db.exec("ALTER TABLE products ADD COLUMN langtext4 TEXT;");
// Standard Admin Initialisieren
const adminUser = db.prepare('SELECT value FROM config WHERE key = ?').get('admin_user');
if (!adminUser) {

View File

@@ -182,11 +182,15 @@ async function parseAndStoreZip(buffer) {
`);
const insertProduct = db.prepare(`
INSERT INTO products (id, text1, text2, group_id, preis1, unit, active)
VALUES (?, ?, ?, ?, ?, ?, 1)
INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
ON CONFLICT(id) DO UPDATE SET
text1=excluded.text1,
text2=excluded.text2,
langtext1=excluded.langtext1,
langtext2=excluded.langtext2,
langtext3=excluded.langtext3,
langtext4=excluded.langtext4,
group_id=excluded.group_id,
preis1=excluded.preis1,
unit=excluded.unit,
@@ -228,6 +232,31 @@ async function parseAndStoreZip(buffer) {
const text2 = item.text2 || item.description || item.additionalText || '';
const groupId = String(item.groupId || item.warengruppe || '');
// Langtexte aus Attributen / Descriptions / Feldern extrahieren
let langtext1 = item.langtext1 || item.Langtext1 || item.longText1 || item.LongText1 || '';
let langtext2 = item.langtext2 || item.Langtext2 || item.longText2 || item.LongText2 || '';
let langtext3 = item.langtext3 || item.Langtext3 || item.longText3 || item.LongText3 || '';
let langtext4 = item.langtext4 || item.Langtext4 || item.longText4 || item.LongText4 || '';
// Falls in descriptions Array oder attributes Array abgelegt
if (Array.isArray(item.descriptions)) {
if (item.descriptions[0]?.text) langtext1 = item.descriptions[0].text;
if (item.descriptions[1]?.text) langtext2 = item.descriptions[1].text;
if (item.descriptions[2]?.text) langtext3 = item.descriptions[2].text;
if (item.descriptions[3]?.text) langtext4 = item.descriptions[3].text;
}
if (Array.isArray(item.attributes)) {
for (const attr of item.attributes) {
const attrName = (attr.name || attr.title || '').toLowerCase();
const attrVal = attr.value || attr.text || '';
if (attrName.includes('langtext 1') || attrName.includes('langtext1')) langtext1 = attrVal;
if (attrName.includes('langtext 2') || attrName.includes('langtext2')) langtext2 = attrVal;
if (attrName.includes('langtext 3') || attrName.includes('langtext3')) langtext3 = attrVal;
if (attrName.includes('langtext 4') || attrName.includes('langtext4')) langtext4 = attrVal;
}
}
// Preis 1 ermitteln (VK1)
let preis1 = 0;
if (Array.isArray(item.prices) && item.prices.length > 0) {
@@ -246,7 +275,7 @@ async function parseAndStoreZip(buffer) {
const unit = item.contentUnit || item.salesUnit || item.unit || '';
if (id) {
insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, unit);
insertProduct.run(id, text1, text2, langtext1, langtext2, langtext3, langtext4, groupId, isNaN(preis1) ? 0 : preis1, unit);
productsCount++;
}
}
@@ -282,9 +311,13 @@ async function parseAndStoreZip(buffer) {
const id = String(item.id || item.articleNo || '');
const text1 = item.text1 || item.name || 'Unbekannt';
const text2 = item.text2 || '';
const langtext1 = item.langtext1 || item.Langtext1 || '';
const langtext2 = item.langtext2 || item.Langtext2 || '';
const langtext3 = item.langtext3 || item.Langtext3 || '';
const langtext4 = item.langtext4 || item.Langtext4 || '';
const groupId = String(item.groupId || '');
let preis1 = parseFloat(String(item.preis1 || item.price || 0).replace(',', '.'));
if (id) { insertProduct.run(id, text1, text2, groupId, isNaN(preis1) ? 0 : preis1, ''); productsCount++; }
if (id) { insertProduct.run(id, text1, text2, langtext1, langtext2, langtext3, langtext4, groupId, isNaN(preis1) ? 0 : preis1, ''); productsCount++; }
}
}
}
@@ -298,11 +331,15 @@ async function parseAndStoreZip(buffer) {
function importJsonData(data) {
const insertProduct = db.prepare(`
INSERT INTO products (id, text1, text2, group_id, preis1, unit, active)
VALUES (?, ?, ?, ?, ?, ?, 1)
INSERT INTO products (id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit, active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
ON CONFLICT(id) DO UPDATE SET
text1=excluded.text1,
text2=excluded.text2,
langtext1=excluded.langtext1,
langtext2=excluded.langtext2,
langtext3=excluded.langtext3,
langtext4=excluded.langtext4,
group_id=excluded.group_id,
preis1=excluded.preis1,
unit=excluded.unit,
@@ -329,6 +366,10 @@ function importJsonData(data) {
String(p.id || Math.random().toString(36).substring(2)),
p.text1 || 'Unbekannt',
p.text2 || '',
p.langtext1 || '',
p.langtext2 || '',
p.langtext3 || '',
p.langtext4 || '',
String(p.groupId || ''),
parseFloat(String(p.preis1 || p.price || 0).replace(',', '.')),
p.unit || ''

View File

@@ -274,6 +274,11 @@
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
cursor: pointer;
}
.card:hover {
border-color: rgba(255, 255, 255, 0.18);
}
.card:active {
@@ -284,13 +289,38 @@
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
margin-bottom: 4px;
}
.card-name-row {
display: flex;
align-items: center;
gap: 6px;
}
.card-name {
font-size: 16px;
font-weight: 700;
color: var(--text);
transition: color 0.2s;
}
.card:hover .card-name {
color: var(--primary);
}
.expand-indicator {
font-size: 12px;
color: var(--text-muted);
transition: transform 0.3s ease, color 0.2s ease;
}
.card:hover .expand-indicator, .card.expanded .expand-indicator {
color: var(--primary);
}
.card.expanded .expand-indicator {
transform: rotate(180deg);
}
.card-price {
@@ -304,10 +334,50 @@
border: 1px solid rgba(245, 158, 11, 0.2);
}
.card-ingredients {
font-size: 12.5px;
color: var(--text-muted);
/* Text 2 - Feste Inhaltsangabe direkt unter dem Namen */
.card-content-desc {
font-size: 13px;
color: var(--text);
opacity: 0.9;
line-height: 1.45;
margin-top: 2px;
}
/* Aufklappbarer Bereich (Langtext 1 bis 4) */
.card-extra-details {
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.35s ease, opacity 0.3s ease, margin-top 0.3s ease;
display: flex;
flex-direction: column;
gap: 6px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
margin-top: 0;
padding-top: 0;
}
/* Bei Hover am Desktop oder Klick/Tap am Handy aufklappen */
.card:hover .card-extra-details, .card.expanded .card-extra-details {
max-height: 350px;
opacity: 1;
margin-top: 10px;
padding-top: 8px;
}
.detail-item {
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
display: flex;
gap: 6px;
}
.detail-item strong {
color: var(--primary);
font-weight: 600;
min-width: 70px;
flex-shrink: 0;
}
.highlight {
@@ -448,7 +518,6 @@
});
}
// Slider Controls (Pfeile + Mouse Drag)
function initSliderControls() {
const slider = document.getElementById('category-swipe');
const btnLeft = document.getElementById('slide-left');
@@ -462,7 +531,6 @@
slider.scrollBy({ left: 160, behavior: 'smooth' });
});
// Mouse Drag Unterstützung am Desktop
let isDown = false;
let startX;
let scrollLeft;
@@ -498,7 +566,11 @@
if (!query) return true;
const t1 = (item.text1 || '').toLowerCase();
const t2 = (item.text2 || '').toLowerCase();
return t1.includes(query) || t2.includes(query);
const l1 = (item.langtext1 || '').toLowerCase();
const l2 = (item.langtext2 || '').toLowerCase();
const l3 = (item.langtext3 || '').toLowerCase();
const l4 = (item.langtext4 || '').toLowerCase();
return t1.includes(query) || t2.includes(query) || l1.includes(query) || l2.includes(query) || l3.includes(query) || l4.includes(query);
});
if (matchingItems.length === 0) return null;
@@ -525,15 +597,33 @@
<section>
<div class="section-title">${escapeHtml(cat.name)} (${cat.items.length})</div>
<div class="menu-grid">
${cat.items.map(item => `
<div class="card">
${cat.items.map(item => {
const hasExtras = item.langtext1 || item.langtext2 || item.langtext3 || item.langtext4;
return `
<div class="card" onclick="this.classList.toggle('expanded')">
<div class="card-top">
<div class="card-name-row">
<div class="card-name">${highlightText(item.text1, query)}</div>
${hasExtras ? '<span class="expand-indicator">▼</span>' : ''}
</div>
<div class="card-price">${parseFloat(item.preis1).toFixed(2).replace('.', ',')} €</div>
</div>
${item.text2 ? `<div class="card-ingredients">${highlightText(item.text2, query)}</div>` : ''}
<!-- Text 2: Feste Inhaltsangabe direkt unter Name -->
${item.text2 ? `<div class="card-content-desc">${highlightText(item.text2, query)}</div>` : ''}
<!-- Aufklappbare Details bei Hover / Klick -->
${hasExtras ? `
<div class="card-extra-details">
${item.langtext1 ? `<div class="detail-item"><strong>Details:</strong> <span>${highlightText(item.langtext1, query)}</span></div>` : ''}
${item.langtext2 ? `<div class="detail-item"><strong>Zutaten:</strong> <span>${highlightText(item.langtext2, query)}</span></div>` : ''}
${item.langtext3 ? `<div class="detail-item"><strong>Hinweise:</strong> <span>${highlightText(item.langtext3, query)}</span></div>` : ''}
${item.langtext4 ? `<div class="detail-item"><strong>Allergene:</strong> <span>${highlightText(item.langtext4, query)}</span></div>` : ''}
</div>
`).join('')}
` : ''}
</div>
`;
}).join('')}
</div>
</section>
`;

View File

@@ -45,7 +45,7 @@ function authMiddleware(req, res, next) {
// 1. Produkte, Gruppen & Theme Settings für die Speisekarte
app.get('/api/menu', (req, res) => {
const groups = db.prepare('SELECT * FROM product_groups ORDER BY sort_order ASC, name ASC').all();
const products = db.prepare('SELECT id, text1, text2, group_id, preis1, unit FROM products WHERE active = 1 ORDER BY sort_order ASC, text1 ASC').all();
const products = db.prepare('SELECT id, text1, text2, langtext1, langtext2, langtext3, langtext4, group_id, preis1, unit FROM products WHERE active = 1 ORDER BY sort_order ASC, text1 ASC').all();
// Globale Einstellungen laden
const configRows = db.prepare('SELECT key, value FROM config').all();