Initial commit

This commit is contained in:
Bjoern Welker
2026-01-30 08:55:14 +01:00
commit 81a1ed7eef
17 changed files with 2824 additions and 0 deletions

621
index.html Normal file
View File

@@ -0,0 +1,621 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Hellas Shop</title>
<style>
:root {
--bg: #0a2036;
--card: #0f2236;
--muted: #9fb1c8;
--text: #f1f4f8;
--line: rgba(255,255,255,.12);
--accent: #f3d52a;
--teal: #2a8a8a;
--ok: #7bd58d;
--bad: #ff6b7d;
--warn: #f0c04a;
--shadow: 0 12px 28px rgba(0,0,0,.28);
--radius: 14px;
--surface: #0f2236;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, "Noto Sans", "Liberation Sans", sans-serif;
background:
radial-gradient(900px 520px at 50% -10%, rgba(255,255,255,.10), transparent 60%),
radial-gradient(900px 600px at 50% 20%, rgba(16,44,70,.85), transparent 65%),
radial-gradient(1200px 700px at 50% 80%, rgba(7,26,44,.95), transparent 70%),
linear-gradient(180deg, #0a2036 0%, #0b243b 45%, #092135 100%);
color: var(--text);
line-height: 1.35;
}
header {
position: sticky;
top: 0;
z-index: 10;
background: rgba(9,24,40,.9);
border-bottom: 1px solid var(--line);
box-shadow: 0 8px 22px rgba(0,0,0,.35);
}
header::before {
content: "";
display: block;
height: 3px;
background: linear-gradient(90deg, var(--teal), rgba(42,138,138,.0));
}
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 18px 16px;
}
.top {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand img {
height: 42px;
width: auto;
display: block;
filter: drop-shadow(0 2px 6px rgba(0,0,0,.4));
}
h1 {
font-family: "Arial Narrow", "Helvetica Neue Condensed", Impact, "Franklin Gothic Medium", "Arial Black", sans-serif;
font-size: 24px;
margin: 0;
letter-spacing: 1px;
text-transform: uppercase;
}
.meta { color: var(--muted); font-size: 12px; letter-spacing: .3px; }
.controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.search {
min-width: 240px; max-width: 460px; flex: 1;
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 999px;
background: rgba(255,255,255,.06);
border: 1px solid var(--line);
box-shadow: 0 1px 0 rgba(255,255,255,.06) inset;
}
.search input {
width: 100%; border: 0; outline: 0; background: transparent;
color: var(--text); font-size: 14px;
}
.pill {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 12px; border-radius: 999px;
background: rgba(255,255,255,.06);
border: 1px solid var(--line);
user-select: none; cursor: pointer;
font-size: 13px; color: var(--text);
box-shadow: 0 2px 0 rgba(0,0,0,.18);
}
.pill input { accent-color: var(--accent); }
main .wrap { padding-top: 14px; padding-bottom: 28px; }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.stat {
border: 1px solid var(--line);
border-radius: 14px;
padding: 10px 12px;
background: linear-gradient(180deg, rgba(18,45,70,.92), rgba(12,31,51,.98));
display: grid;
gap: 4px;
box-shadow: var(--shadow);
}
.stat .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .8px; }
.stat .value { font-size: 18px; font-weight: 700; letter-spacing: .3px; color: var(--accent); }
.grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.card-tile {
border: 1px solid rgba(255,255,255,.08);
border-radius: 16px;
background: rgba(12,31,51,.92);
box-shadow: 0 10px 22px rgba(0,0,0,.28);
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform .12s ease, box-shadow .12s ease;
}
.card-tile:hover { transform: translateY(-1px); box-shadow: 0 14px 26px rgba(0,0,0,.32); }
.card-media {
width: 100%;
aspect-ratio: 1 / 1;
background: rgba(0,0,0,.2);
display: flex;
align-items: center;
justify-content: center;
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: .6px;
}
.card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-body { padding: 12px 12px 10px; display: grid; gap: 6px; }
.card-title {
font-family: "Arial Narrow", "Helvetica Neue Condensed", Impact, "Franklin Gothic Medium", "Arial Black", sans-serif;
font-size: 16px; font-weight: 600; letter-spacing: .6px;
}
.card-price { color: var(--accent); font-weight: 800; font-size: 14px; }
.card-sub { color: var(--muted); font-size: 12px; }
.card-actions { padding: 0 12px 12px; display: flex; justify-content: flex-end; }
.detail-btn { width: 100%; }
.size-list { display: grid; gap: 8px; padding: 10px 2px 2px; }
.size-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: center; padding: 8px 10px; border-radius: 10px; }
.size-row:nth-child(odd) { background: rgba(255,255,255,.04); }
.size-meta { font-size: 13px; color: var(--text); }
.size-meta strong { font-weight: 700; letter-spacing: .2px; }
.art { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.art .name {
font-family: "Arial Narrow", "Helvetica Neue Condensed", Impact, "Franklin Gothic Medium", "Arial Black", sans-serif;
font-size: 18px; font-weight: 600; letter-spacing: .6px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.art .sub { font-size: 12px; color: var(--muted); }
.badges { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.badge {
font-size: 12px; padding: 6px 10px; border-radius: 10px;
border: 1px solid rgba(255,255,255,.12);
background: rgba(255,255,255,.05);
color: var(--text);
white-space: nowrap;
font-weight: 700;
letter-spacing: .2px;
}
.thumb {
width: 64px;
height: 64px;
border-radius: 12px;
background:
linear-gradient(135deg, rgba(255,255,255,.08), rgba(0,0,0,.2)),
radial-gradient(circle at 30% 30%, rgba(255,255,255,.25), transparent 40%);
border: 1px solid rgba(255,255,255,.12);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .6px;
flex: 0 0 auto;
}
.price {
margin-top: 4px;
font-size: 12px;
color: var(--accent);
font-weight: 700;
letter-spacing: .2px;
}
.content {
border-top: 1px solid rgba(255,255,255,.08);
padding: 10px 12px 12px 12px;
background: rgba(6,16,28,.3);
}
.table-wrap { overflow-x: auto; }
.table-wrap table { min-width: 520px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; overflow: hidden; }
th, td { padding: 10px 8px; border-bottom: 1px solid rgba(255,255,255,.06); text-align: right; vertical-align: middle; font-variant-numeric: tabular-nums; }
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); font-weight: 600; }
tr:last-child td { border-bottom: 0; }
thead th { background: rgba(0,0,0,.12); }
tbody tr:nth-child(odd) { background: rgba(255,255,255,.02); }
.delta-pos { color: rgba(123,213,141,.98); font-weight: 700; }
.delta-neg { color: rgba(255,107,125,.98); font-weight: 700; }
.muted { color: var(--muted); }
.small { font-size: 12px; }
.footer {
margin-top: 14px; color: var(--muted); font-size: 12px;
display: flex; gap: 10px; flex-wrap: wrap; justify-content: space-between;
border-top: 1px dashed rgba(255,255,255,.12);
padding-top: 12px;
}
.empty, .error {
border: 1px dashed rgba(255,255,255,.2);
border-radius: var(--radius);
padding: 16px;
color: var(--muted);
text-align: center;
}
.error { color: #ffd6de; border-color: rgba(255,107,125,.4); }
.order-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 10px;
border: 1px solid var(--line);
background: rgba(255,255,255,.06);
color: var(--text);
cursor: pointer;
font-size: 12px;
}
.modal {
position: fixed;
inset: 0;
background: rgba(5,12,20,.6);
display: none;
align-items: flex-start;
justify-content: center;
padding: 84px 16px 24px;
z-index: 50;
opacity: 0;
transition: opacity .18s ease;
}
.modal.open { display: flex; opacity: 1; }
.modal-card {
width: 100%;
max-width: 520px;
border: 1px solid var(--line);
border-radius: 14px;
background: linear-gradient(180deg, rgba(18,45,70,.92), rgba(12,31,51,.98));
box-shadow: 0 16px 28px rgba(0,0,0,.35);
padding: 14px;
max-height: calc(100vh - 80px);
overflow: auto;
transform: scale(.98);
transition: transform .18s ease;
}
.modal.open .modal-card { transform: scale(1); }
.modal-card h3 { margin: 0 0 10px; }
.detail-header { display: grid; gap: 8px; }
.detail-title { font-size: 20px; font-weight: 700; letter-spacing: .4px; }
.detail-price { color: var(--accent); font-weight: 800; }
.detail-media {
width: 100%;
aspect-ratio: 4 / 3;
border-radius: 12px;
overflow: hidden;
background: rgba(0,0,0,.2);
}
.detail-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.modal-card { position: relative; }
.close-x {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,.35);
background: rgba(8,16,28,.85);
color: var(--text);
font-size: 16px;
line-height: 1;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,.35);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.form-grid label { display: grid; gap: 6px; font-size: 12px; color: var(--muted); }
.form-grid input, .form-grid textarea {
background: rgba(255,255,255,.06);
border: 1px solid var(--line);
color: var(--text);
padding: 8px 10px;
border-radius: 10px;
font-size: 13px;
}
.form-actions { margin-top: 12px; display: flex; gap: 10px; justify-content: flex-end; }
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255,255,255,.06);
color: var(--text); cursor: pointer;
}
.btn.accent { background: var(--accent); color: #071320; border-color: transparent; font-weight: 700; }
.img-modal .modal-card { max-width: 720px; padding: 10px; }
.img-modal img { width: 100%; height: auto; border-radius: 12px; display: block; }
.order-modal { z-index: 60; }
@media (max-width: 700px) {
.top { align-items: flex-start; }
.brand { width: 100%; }
.controls { width: 100%; }
.search { min-width: 100%; }
.stats { grid-template-columns: 1fr; }
.card-media { aspect-ratio: 4 / 3; }
.order-btn { width: 100%; justify-content: center; }
}
@media (max-width: 640px) { .grid { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<header>
<div class="wrap">
<div class="top">
<div class="brand">
<img src="logo.png" alt="Hellas 1899 Logo" />
<div>
<h1>Shop</h1>
<div class="meta" id="meta">Stand: </div>
</div>
</div>
<div class="controls">
<div class="search" role="search">
<span aria-hidden="true">🔎</span>
<input id="q" type="search" placeholder="Artikel suchen… (z.B. Badehose, Hoodie, Kappe)" />
</div>
</div>
</div>
</div>
</header>
<main>
<div class="wrap">
<div id="stats" class="stats" aria-live="polite"></div>
<div id="grid" class="grid" aria-live="polite"></div>
<div class="footer">
<div>Tip: Auf einen Artikel klicken, um die GrößenTabelle zu öffnen.</div>
</div>
</div>
</main>
<div id="orderModal" class="modal order-modal" role="dialog" aria-modal="true">
<div class="modal-card">
<button class="close-x" id="orderCloseX" aria-label="Schließen">×</button>
<h3>Bestellung</h3>
<form id="orderForm">
<div class="form-grid">
<label>Artikel
<input id="fArtikel" name="artikel" readonly />
</label>
<label>Größe
<input id="fGroesse" name="groesse" readonly />
</label>
<label>Menge
<input id="fMenge" name="menge" type="number" min="1" value="1" required />
</label>
<label>Name
<input id="fName" name="name" required />
</label>
<label>Handy
<input id="fHandy" name="handy" required />
</label>
<label>Mannschaft
<input id="fMannschaft" name="mannschaft" placeholder="z. B. U12" required />
</label>
<label style="grid-column: 1 / -1;">Notiz
<textarea id="fNotiz" name="notiz" rows="3"></textarea>
</label>
</div>
<div class="form-actions">
<button type="button" class="btn" id="orderCancel">Abbrechen</button>
<button type="submit" class="btn accent">Bestellen</button>
</div>
</form>
</div>
</div>
<div id="imgModal" class="modal img-modal" role="dialog" aria-modal="true">
<div class="modal-card">
<img id="imgPreview" src="" alt="Artikelbild" />
</div>
</div>
<div id="detailModal" class="modal" role="dialog" aria-modal="true">
<div class="modal-card">
<button class="close-x" id="detailCloseX" aria-label="Schließen">×</button>
<div class="detail-header">
<div id="detailMedia" class="detail-media"></div>
<div class="detail-title" id="detailTitle"></div>
<div class="detail-price" id="detailPrice"></div>
<div class="card-sub" id="detailSizes"></div>
</div>
<div class="size-list" id="detailSizeList"></div>
<div class="form-actions">
<button type="button" class="btn" id="detailClose">Schließen</button>
</div>
</div>
</div>
<script>
// Proxy der WaWiApp (kein APIKey im Browser nötig).
const API_URL = "/wawi/proxy/bestand";
let DATA = [];
const DATA_MAP = new Map();
function fmt(v) {
if (v === null || v === undefined || Number.isNaN(v)) return "";
return String(v);
}
function badge(label, value, cls="") {
if (value === null || value === undefined) return "";
return `<span class="badge ${cls}">${label}: <strong>${fmt(value)}</strong></span>`;
}
function deltaClass(d) {
if (d === null || d === undefined) return "";
if (d > 0) return "delta-pos";
if (d < 0) return "delta-neg";
return "";
}
function hasDiff(item) {
const t = item.totals || {};
return (t.abweichung ?? 0) !== 0 || (t.fehlbestand ?? 0) !== 0;
}
function render(items) {
const grid = document.getElementById("grid");
const stats = document.getElementById("stats");
const totals = items.reduce((acc, item) => {
const t = item.totals || {};
acc.count += 1;
acc.soll += t.soll ?? 0;
return acc;
}, { count: 0, soll: 0 });
stats.innerHTML = `
<div class="stat"><div class="label">Artikel</div><div class="value">${fmt(totals.count)}</div></div>
<div class="stat"><div class="label">Bestand gesamt</div><div class="value">${fmt(totals.soll)}</div></div>
`;
if (!items.length) {
grid.innerHTML = `<div class="empty">Keine Treffer. (Suchbegriff anpassen)</div>`;
return;
}
grid.innerHTML = items.map((item, idx) => {
const t = item.totals || {};
const diff = t.abweichung ?? 0;
const fb = t.fehlbestand ?? null;
let statusCls = "ok";
if ((diff ?? 0) !== 0) statusCls = "bad";
else if ((fb ?? 0) !== 0) statusCls = "warn";
const statusText = ((diff ?? 0) === 0 && (fb ?? 0) === 0) ? "OK" : "Prüfen";
const price = Number(item.preis) || 0;
const priceText = price > 0 ? `${price.toFixed(2)}` : "Preis auf Anfrage";
const img = (item.bild_url || "").trim();
return `
<div class="card-tile">
<div class="card-media">
${img ? `<button class="thumb-btn" data-img="${img}" aria-label="Bild vergrößern" title="Bild vergrößern" style="width:100%;height:100%;background-image:url('${img}');background-size:cover;background-position:center;border:0;cursor:pointer;"></button>` : "Bild"}
</div>
<div class="card-body">
<div class="card-title">${item.artikel}</div>
<div class="card-price">${priceText}</div>
<div class="card-sub">${item.rows.length} Größen</div>
</div>
<div class="card-actions">
<button class="order-btn detail-btn" data-artikel="${item.artikel}" data-preis="${priceText}" data-img="${img}">Bestellen / Details</button>
</div>
</div>
`;
}).join("");
}
function applyFilters() {
const q = (document.getElementById("q").value || "").trim().toLowerCase();
let items = DATA.slice();
if (q) {
items = items.filter(it => (it.artikel || "").toLowerCase().includes(q));
}
render(items);
}
async function loadData() {
const res = await fetch(API_URL);
if (!res.ok) {
const grid = document.getElementById("grid");
grid.innerHTML = `<div class="error">API Fehler (${res.status}). Bitte APIKey prüfen.</div>`;
return;
}
DATA = await res.json();
DATA_MAP.clear();
DATA.forEach(item => DATA_MAP.set(item.artikel, item));
document.getElementById("meta").textContent = `Stand: ${new Date().toLocaleString("de-DE")}`;
applyFilters();
}
document.getElementById("q").addEventListener("input", applyFilters);
const modal = document.getElementById("orderModal");
const form = document.getElementById("orderForm");
const setField = (id, v) => document.getElementById(id).value = v || "";
document.addEventListener("click", (e) => {
const imgBtn = e.target.closest(".thumb-btn");
if (imgBtn) {
document.getElementById("imgPreview").src = imgBtn.dataset.img;
document.getElementById("imgModal").classList.add("open");
return;
}
const detailBtn = e.target.closest(".detail-btn");
if (detailBtn) {
const artikel = detailBtn.dataset.artikel;
const item = DATA_MAP.get(artikel);
if (!item) return;
const img = (detailBtn.dataset.img || "").trim();
const media = document.getElementById("detailMedia");
media.innerHTML = img ? `<img src="${img}" alt="${artikel}">` : "";
document.getElementById("detailTitle").textContent = artikel;
document.getElementById("detailPrice").textContent = detailBtn.dataset.preis || "Preis auf Anfrage";
document.getElementById("detailSizes").textContent = `${item.rows.length} Größen`;
const list = item.rows.map(r => `
<div class="size-row">
<div class="size-meta"><strong>${fmt(r.groesse)}</strong> · Bestand: ${fmt(r.gezaehlt)}</div>
${(Number(r.gezaehlt) || 0) > 0 ? `<button class="order-btn" data-artikel="${item.artikel}" data-groesse="${fmt(r.groesse)}">Bestellen</button>` : ""}
</div>
`).join("");
document.getElementById("detailSizeList").innerHTML = list;
document.getElementById("detailModal").classList.add("open");
return;
}
const btn = e.target.closest(".order-btn");
if (!btn) return;
setField("fArtikel", btn.dataset.artikel);
setField("fGroesse", btn.dataset.groesse);
setField("fMenge", 1);
modal.classList.add("open");
});
document.getElementById("orderCancel").addEventListener("click", () => {
modal.classList.remove("open");
if (document.getElementById("detailModal").classList.contains("open")) {
document.getElementById("detailModal").querySelector(".size-row")?.scrollIntoView({ block: "nearest" });
}
});
document.getElementById("orderCloseX").addEventListener("click", () => {
modal.classList.remove("open");
});
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.classList.remove("open");
});
document.getElementById("imgModal").addEventListener("click", (e) => {
if (e.target.id === "imgModal") e.currentTarget.classList.remove("open");
});
document.getElementById("detailClose").addEventListener("click", () => {
document.getElementById("detailModal").classList.remove("open");
});
document.getElementById("detailCloseX").addEventListener("click", () => {
document.getElementById("detailModal").classList.remove("open");
});
document.getElementById("detailModal").addEventListener("click", (e) => {
if (e.target.id === "detailModal") e.currentTarget.classList.remove("open");
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = Object.fromEntries(new FormData(form).entries());
const res = await fetch("/wawi/order", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (res.ok) {
alert("Bestellung gesendet.");
modal.classList.remove("open");
document.getElementById("detailModal").classList.remove("open");
form.reset();
setField("fMenge", 1);
} else {
alert("Fehler beim Senden der Bestellung.");
}
});
loadData();
</script>
</body>
</html>