Files
getrankekarte/public/index.html
DanielS 1b0726deef
All checks were successful
Build and Deploy / build (push) Successful in 54s
feat: Text 2 als feste Inhaltsangabe und Langtext 1-4 als aufklappbare Details (Hover/Touch)
2026-09-22 23:36:12 +02:00

680 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title id="html-title">GETRÄNKEKARTE</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<style id="dynamic-theme">
:root {
--bg: #090b10;
--card-bg: rgba(22, 27, 34, 0.75);
--card-border: rgba(255, 255, 255, 0.08);
--primary: #f59e0b;
--primary-glow: rgba(245, 158, 11, 0.25);
--accent: #ec4899;
--accent-glow: rgba(236, 72, 153, 0.25);
--text: #f8fafc;
--text-muted: #94a3b8;
--tag-bg: rgba(245, 158, 11, 0.12);
}
</style>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
background-color: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 20px 16px 40px;
position: relative;
overflow-x: hidden;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Ambient Glow */
body::before {
content: "";
position: fixed;
top: -100px;
left: 50%;
transform: translateX(-50%);
width: 400px;
height: 400px;
background: radial-gradient(circle, var(--accent-glow) 0%, var(--primary-glow) 50%, transparent 70%);
filter: blur(80px);
z-index: 0;
pointer-events: none;
}
.container {
width: 100%;
max-width: 480px;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
header {
text-align: center;
padding: 20px 0 6px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
font-size: 11px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--primary);
margin-bottom: 12px;
backdrop-filter: blur(10px);
}
h1 {
font-family: 'Space Grotesk', sans-serif;
font-size: 32px;
font-weight: 700;
letter-spacing: -0.5px;
background: linear-gradient(135deg, var(--text) 30%, var(--text-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
header p {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
}
/* Volltextsuche */
.search-wrapper {
position: relative;
width: 100%;
}
.search-input {
width: 100%;
padding: 14px 42px 14px 44px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 14px;
color: var(--text);
font-size: 14px;
outline: none;
backdrop-filter: blur(16px);
transition: border-color 0.2s, box-shadow 0.2s;
font-family: inherit;
}
.search-input:focus {
border-color: var(--primary);
box-shadow: 0 0 16px var(--primary-glow);
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: var(--text-muted);
pointer-events: none;
}
.clear-search {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
font-size: 16px;
cursor: pointer;
display: none;
}
/* Interaktiver Slider / Swipe Container */
.slider-nav-container {
position: relative;
display: flex;
align-items: center;
margin: 0 -16px;
padding: 0 8px;
}
.slider-arrow {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
z-index: 2;
flex-shrink: 0;
backdrop-filter: blur(10px);
transition: all 0.2s;
}
.slider-arrow:hover {
background: var(--primary);
color: var(--bg);
border-color: var(--primary);
}
.category-swipe {
display: flex;
gap: 8px;
overflow-x: auto;
scroll-behavior: smooth;
padding: 6px 8px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
flex: 1;
cursor: grab;
user-select: none;
}
.category-swipe:active {
cursor: grabbing;
}
.category-swipe::-webkit-scrollbar {
display: none;
}
.category-pill {
flex: 0 0 auto;
padding: 8px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
color: var(--text-muted);
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
cursor: pointer;
white-space: nowrap;
transition: all 0.25s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.category-pill:hover {
color: var(--text);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-1px);
}
.category-pill.active {
background: var(--primary);
color: var(--bg);
border-color: var(--primary);
box-shadow: 0 0 14px var(--primary-glow);
transform: scale(1.03);
}
.section-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--primary);
display: flex;
align-items: center;
gap: 8px;
margin: 16px 0 12px;
}
.section-title::after {
content: "";
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--card-border), transparent);
}
.menu-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 16px;
backdrop-filter: blur(16px);
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 {
transform: scale(0.985);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
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 {
font-family: 'Space Grotesk', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--primary);
background: rgba(245, 158, 11, 0.1);
padding: 4px 10px;
border-radius: 8px;
border: 1px solid rgba(245, 158, 11, 0.2);
}
/* 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 {
background: rgba(245, 158, 11, 0.3);
color: var(--text);
padding: 0 2px;
border-radius: 3px;
}
.loading-spinner, .no-results {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
font-size: 14px;
}
footer {
text-align: center;
padding-top: 16px;
font-size: 12px;
color: var(--text-muted);
}
footer a {
color: var(--text-muted);
text-decoration: none;
transition: color 0.2s;
}
footer a:hover {
color: var(--primary);
}
</style>
</head>
<body>
<div class="container">
<header>
<div id="site-badge" class="badge">✦ EXKLUSIVE AUSWAHL ✦</div>
<h1 id="site-title">GETRÄNKEKARTE</h1>
<p id="site-subtitle">Frisch zubereitete Drinks, Spezialitäten & Erfrischungen</p>
</header>
<!-- Volltextsuche -->
<div class="search-wrapper">
<span class="search-icon">🔍</span>
<input type="text" id="search-input" class="search-input" placeholder="Getränk oder Zutat suchen..." autocomplete="off" />
<button id="clear-search" class="clear-search">✕</button>
</div>
<!-- Warengruppen Slider mit Pfeilen & Touch Drag -->
<div class="slider-nav-container">
<button id="slide-left" class="slider-arrow" aria-label="Zurück">‹</button>
<div id="category-swipe" class="category-swipe">
<button class="category-pill active" data-id="all">Alle Artikel</button>
</div>
<button id="slide-right" class="slider-arrow" aria-label="Weiter">›</button>
</div>
<!-- Getränkeliste -->
<div id="menu-container">
<div class="loading-spinner">Getränkekarte wird geladen...</div>
</div>
<footer>
<p id="site-footer">© Bar & Lounge • Live POS Sync</p>
<p style="margin-top: 6px;"><a href="/admin">⚙️ Admin Bereich</a></p>
</footer>
</div>
<script>
let allCategories = [];
let activeCategoryId = 'all';
let searchQuery = '';
async function loadMenu() {
const container = document.getElementById('menu-container');
try {
const res = await fetch('/api/menu');
const data = await res.json();
allCategories = data.categories || [];
if (data.settings) {
applySettings(data.settings);
}
renderCategoryPills();
initSliderControls();
renderMenu();
} catch (err) {
container.innerHTML = '<div class="card" style="color:#ef4444; text-align:center;">Fehler beim Laden der Speisekarte.</div>';
}
}
function applySettings(s) {
if (s.site_title) {
document.getElementById('site-title').textContent = s.site_title;
document.getElementById('html-title').textContent = s.site_title;
}
if (s.site_subtitle) document.getElementById('site-subtitle').textContent = s.site_subtitle;
if (s.site_badge) document.getElementById('site-badge').textContent = s.site_badge;
if (s.site_footer) document.getElementById('site-footer').textContent = s.site_footer;
const root = document.documentElement;
if (s.theme_bg) root.style.setProperty('--bg', s.theme_bg);
if (s.theme_card_bg) root.style.setProperty('--card-bg', s.theme_card_bg);
if (s.theme_primary) {
root.style.setProperty('--primary', s.theme_primary);
root.style.setProperty('--primary-glow', s.theme_primary + '40');
}
if (s.theme_accent) {
root.style.setProperty('--accent', s.theme_accent);
root.style.setProperty('--accent-glow', s.theme_accent + '40');
}
if (s.theme_text) root.style.setProperty('--text', s.theme_text);
if (s.theme_text_muted) root.style.setProperty('--text-muted', s.theme_text_muted);
}
function renderCategoryPills() {
const swipeEl = document.getElementById('category-swipe');
let pillsHtml = `<button class="category-pill ${activeCategoryId === 'all' ? 'active' : ''}" data-id="all">Alle Artikel</button>`;
for (const cat of allCategories) {
pillsHtml += `<button class="category-pill ${activeCategoryId === cat.id ? 'active' : ''}" data-id="${cat.id}">${escapeHtml(cat.name)}</button>`;
}
swipeEl.innerHTML = pillsHtml;
swipeEl.querySelectorAll('.category-pill').forEach(btn => {
btn.addEventListener('click', () => {
swipeEl.querySelectorAll('.category-pill').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeCategoryId = btn.dataset.id;
btn.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
renderMenu();
});
});
}
function initSliderControls() {
const slider = document.getElementById('category-swipe');
const btnLeft = document.getElementById('slide-left');
const btnRight = document.getElementById('slide-right');
btnLeft.addEventListener('click', () => {
slider.scrollBy({ left: -160, behavior: 'smooth' });
});
btnRight.addEventListener('click', () => {
slider.scrollBy({ left: 160, behavior: 'smooth' });
});
let isDown = false;
let startX;
let scrollLeft;
slider.addEventListener('mousedown', (e) => {
isDown = true;
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', () => { isDown = false; });
slider.addEventListener('mouseup', () => { isDown = false; });
slider.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - slider.offsetLeft;
const walk = (x - startX) * 1.5;
slider.scrollLeft = scrollLeft - walk;
});
}
function renderMenu() {
const container = document.getElementById('menu-container');
const query = searchQuery.trim().toLowerCase();
let filteredCategories = allCategories.map(cat => {
if (activeCategoryId !== 'all' && cat.id !== activeCategoryId) {
return null;
}
const matchingItems = cat.items.filter(item => {
if (!query) return true;
const t1 = (item.text1 || '').toLowerCase();
const t2 = (item.text2 || '').toLowerCase();
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;
return {
...cat,
items: matchingItems
};
}).filter(Boolean);
if (filteredCategories.length === 0) {
container.innerHTML = `
<div class="card no-results">
<p style="font-size: 16px; margin-bottom: 6px;">Keine Treffer gefunden</p>
<p style="font-size: 13px; color: var(--text-muted);">Versuche einen anderen Suchbegriff oder eine andere Kategorie.</p>
</div>
`;
return;
}
let html = '';
for (const cat of filteredCategories) {
html += `
<section>
<div class="section-title">${escapeHtml(cat.name)} (${cat.items.length})</div>
<div class="menu-grid">
${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>
<!-- 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>
` : ''}
</div>
`;
}).join('')}
</div>
</section>
`;
}
container.innerHTML = html;
}
// Suchfeld Listener
const searchInput = document.getElementById('search-input');
const clearBtn = document.getElementById('clear-search');
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value;
clearBtn.style.display = searchQuery.length > 0 ? 'block' : 'none';
renderMenu();
});
clearBtn.addEventListener('click', () => {
searchInput.value = '';
searchQuery = '';
clearBtn.style.display = 'none';
searchInput.focus();
renderMenu();
});
function highlightText(text, query) {
if (!text) return '';
const safe = escapeHtml(text);
if (!query) return safe;
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
return safe.replace(regex, '<span class="highlight">$1</span>');
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>'"]/g, tag => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[tag] || tag));
}
function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}
loadMenu();
</script>
</body>
</html>