Warenkorb/Produktseite: nie mehr in den Warenkorb legen können als auf Lager ist
- Produktseite: Mengen-Stepper und manuelle Eingabe sind jetzt auf den tatsächlichen Lagerbestand begrenzt (zählt bereits im Warenkorb liegende Menge desselben Produkts mit). Ist das Maximum schon erreicht, wird der "In den Warenkorb"-Button deaktiviert und zeigt "Maximale verfügbare Menge ist bereits im Warenkorb"; bei wenig Rest- bestand (≤5) erscheint stattdessen "Nur noch X auf Lager". - Warenkorb-Seite: der "+"-Button bei der Mengenauswahl ist ebenfalls auf den Lagerbestand begrenzt. - Als zusätzliche Absicherung greift das Limit jetzt auch direkt in addToCart()/updateQuantity() selbst (cart.ts) — unabhängig davon, welche Oberfläche die Funktion aufruft, kann so nie mehr Menge als Bestand ins localStorage gelangen. Verifiziert per Puppeteer an einem Produkt mit Bestand 2: 6× auf "+" klicken bleibt bei 2, manuelles Eintippen von 999 wird auf 2 geklemmt, nach dem Hinzufügen ist der Button deaktiviert, der "+"-Button im Warenkorb erhöht nicht weiter — keine Konsolenfehler.
This commit is contained in:
@@ -20,6 +20,7 @@ export const ui = {
|
|||||||
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handgemacht", ausverkauft: "Ausverkauft" },
|
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handgemacht", ausverkauft: "Ausverkauft" },
|
||||||
common: {
|
common: {
|
||||||
addToCart: "In den Warenkorb", soldOut: "Ausverkauft", addedToCart: "Zum Warenkorb hinzugefügt ✓",
|
addToCart: "In den Warenkorb", soldOut: "Ausverkauft", addedToCart: "Zum Warenkorb hinzugefügt ✓",
|
||||||
|
onlyXLeft: (n: string) => `Nur noch ${n} auf Lager`, maxInCart: "Maximale verfügbare Menge ist bereits im Warenkorb",
|
||||||
browseShop: "Jetzt stöbern", viewAll: "Alle ansehen →", viewAllSale: "Alle Angebote →",
|
browseShop: "Jetzt stöbern", viewAll: "Alle ansehen →", viewAllSale: "Alle Angebote →",
|
||||||
toShop: "Zum Shop →", moreAboutMe: "Mehr über mich →", discoverNow: "Jetzt entdecken",
|
toShop: "Zum Shop →", moreAboutMe: "Mehr über mich →", discoverNow: "Jetzt entdecken",
|
||||||
vatNote: "Gemäß §19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.",
|
vatNote: "Gemäß §19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.",
|
||||||
@@ -202,6 +203,7 @@ export const ui = {
|
|||||||
badge: { neu: "New", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handmade", ausverkauft: "Sold out" },
|
badge: { neu: "New", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handmade", ausverkauft: "Sold out" },
|
||||||
common: {
|
common: {
|
||||||
addToCart: "Add to cart", soldOut: "Sold out", addedToCart: "Added to cart ✓",
|
addToCart: "Add to cart", soldOut: "Sold out", addedToCart: "Added to cart ✓",
|
||||||
|
onlyXLeft: (n: string) => `Only ${n} left in stock`, maxInCart: "The maximum available quantity is already in your cart",
|
||||||
browseShop: "Browse now", viewAll: "View all →", viewAllSale: "View all offers →",
|
browseShop: "Browse now", viewAll: "View all →", viewAllSale: "View all offers →",
|
||||||
toShop: "To the shop →", moreAboutMe: "More about me →", discoverNow: "Discover now",
|
toShop: "To the shop →", moreAboutMe: "More about me →", discoverNow: "Discover now",
|
||||||
vatNote: "Under §19 UStG (German small-business rule) no VAT is charged or shown.",
|
vatNote: "Under §19 UStG (German small-business rule) no VAT is charged or shown.",
|
||||||
@@ -384,6 +386,7 @@ export const ui = {
|
|||||||
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Aktion", handgemacht: "Handgmacht", ausverkauft: "Usverkauft" },
|
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Aktion", handgemacht: "Handgmacht", ausverkauft: "Usverkauft" },
|
||||||
common: {
|
common: {
|
||||||
addToCart: "In Warenchorb", soldOut: "Usverkauft", addedToCart: "Zum Warenchorb dezuegfüegt ✓",
|
addToCart: "In Warenchorb", soldOut: "Usverkauft", addedToCart: "Zum Warenchorb dezuegfüegt ✓",
|
||||||
|
onlyXLeft: (n: string) => `Nume no ${n} a Lager`, maxInCart: "D'maximal verfügbari Mengi isch scho im Warenchorb",
|
||||||
browseShop: "Jetzt use luege", viewAll: "Alles aluege →", viewAllSale: "Alli Aktione →",
|
browseShop: "Jetzt use luege", viewAll: "Alles aluege →", viewAllSale: "Alli Aktione →",
|
||||||
toShop: "Zum Shop →", moreAboutMe: "Meh über mich →", discoverNow: "Jetzt aluege",
|
toShop: "Zum Shop →", moreAboutMe: "Meh über mich →", discoverNow: "Jetzt aluege",
|
||||||
vatNote: "Gemäss §19 UStG (dütsches Rächt) wird kei Umsatzsteuer berechnet und usgwiese.",
|
vatNote: "Gemäss §19 UStG (dütsches Rächt) wird kei Umsatzsteuer berechnet und usgwiese.",
|
||||||
@@ -566,6 +569,7 @@ export const ui = {
|
|||||||
badge: { neu: "Nouveau", bestseller: "Best-seller", sale: "Soldes", handgemacht: "Fait main", ausverkauft: "Épuisé" },
|
badge: { neu: "Nouveau", bestseller: "Best-seller", sale: "Soldes", handgemacht: "Fait main", ausverkauft: "Épuisé" },
|
||||||
common: {
|
common: {
|
||||||
addToCart: "Ajouter au panier", soldOut: "Épuisé", addedToCart: "Ajouté au panier ✓",
|
addToCart: "Ajouter au panier", soldOut: "Épuisé", addedToCart: "Ajouté au panier ✓",
|
||||||
|
onlyXLeft: (n: string) => `Plus que ${n} en stock`, maxInCart: "La quantité maximale disponible est déjà dans votre panier",
|
||||||
browseShop: "Découvrir la boutique", viewAll: "Tout voir →", viewAllSale: "Toutes les offres →",
|
browseShop: "Découvrir la boutique", viewAll: "Tout voir →", viewAllSale: "Toutes les offres →",
|
||||||
toShop: "Vers la boutique →", moreAboutMe: "En savoir plus sur moi →", discoverNow: "Découvrir maintenant",
|
toShop: "Vers la boutique →", moreAboutMe: "En savoir plus sur moi →", discoverNow: "Découvrir maintenant",
|
||||||
vatNote: "Conformément à l'§19 UStG (droit allemand), aucune TVA n'est calculée ni indiquée.",
|
vatNote: "Conformément à l'§19 UStG (droit allemand), aucune TVA n'est calculée ni indiquée.",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const felder: [string, string | undefined][] = [
|
|||||||
<label for="qty">{t.common.quantity}</label>
|
<label for="qty">{t.common.quantity}</label>
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button type="button" id="qty-dec" aria-label="−">−</button>
|
<button type="button" id="qty-dec" aria-label="−">−</button>
|
||||||
<input type="number" id="qty" min="1" value="1" inputmode="numeric" />
|
<input type="number" id="qty" min="1" max={product.bestand} value={product.bestand > 0 ? 1 : 0} inputmode="numeric" data-bestand={product.bestand} />
|
||||||
<button type="button" id="qty-inc" aria-label="+">+</button>
|
<button type="button" id="qty-inc" aria-label="+">+</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -99,10 +99,12 @@ const felder: [string, string | undefined][] = [
|
|||||||
data-slug={product.slug}
|
data-slug={product.slug}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-preis={finalPrice}
|
data-preis={finalPrice}
|
||||||
|
data-bestand={product.bestand}
|
||||||
>
|
>
|
||||||
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="small" id="stock-hint" style="display:none;"></p>
|
||||||
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
||||||
|
|
||||||
{felder.some(([, v]) => v) && (
|
{felder.some(([, v]) => v) && (
|
||||||
@@ -131,22 +133,78 @@ const felder: [string, string | undefined][] = [
|
|||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script define:vars={{ onlyLeftTemplate: t.common.onlyXLeft("__N__"), maxInCartText: t.common.maxInCart, soldOutText: t.common.soldOut, addToCartText: t.common.addToCart }}>
|
||||||
|
window.__productStockVars = { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText };
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import { addToCart } from "../../../scripts/cart";
|
import { addToCart, getCart } from "../../../scripts/cart";
|
||||||
|
const { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText } = (window as any).__productStockVars;
|
||||||
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
||||||
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
||||||
const confirm = document.getElementById("add-confirm")!;
|
const confirm = document.getElementById("add-confirm")!;
|
||||||
|
const stockHint = document.getElementById("stock-hint")!;
|
||||||
|
const bestand = Number(qtyInput.dataset.bestand || 0);
|
||||||
|
const slug = btn?.dataset.slug;
|
||||||
|
|
||||||
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist — zählt dabei mit,
|
||||||
|
// was von diesem Produkt schon im Warenkorb liegt (nicht nur die aktuelle Eingabe hier).
|
||||||
|
function bereitsImWarenkorb() {
|
||||||
|
return getCart().find((i) => i.slug === slug)?.menge ?? 0;
|
||||||
|
}
|
||||||
|
function maxJetztHinzufuegbar() {
|
||||||
|
return Math.max(0, bestand - bereitsImWarenkorb());
|
||||||
|
}
|
||||||
|
function clamp() {
|
||||||
|
const max = maxJetztHinzufuegbar();
|
||||||
|
let val = Math.max(0, Math.floor(Number(qtyInput.value) || 0));
|
||||||
|
if (val > max) val = max;
|
||||||
|
if (val < 1 && max > 0) val = 1;
|
||||||
|
qtyInput.value = String(val);
|
||||||
|
qtyInput.max = String(max);
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
if (bestand === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = soldOutText;
|
||||||
|
} else if (max === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = maxInCartText;
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = maxInCartText;
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = addToCartText;
|
||||||
|
if (max <= 5) {
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = onlyLeftTemplate.replace("__N__", String(max));
|
||||||
|
} else {
|
||||||
|
stockHint.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
clamp();
|
||||||
|
|
||||||
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Number(qtyInput.value) + 1);
|
qtyInput.value = String(Number(qtyInput.value) + 1);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
qtyInput.addEventListener("input", clamp);
|
||||||
|
qtyInput.addEventListener("change", clamp);
|
||||||
|
|
||||||
btn?.addEventListener("click", () => {
|
btn?.addEventListener("click", () => {
|
||||||
const { slug, name, preis } = btn.dataset;
|
const menge = clamp();
|
||||||
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
|
if (menge < 1) return;
|
||||||
|
const { name, preis } = btn.dataset;
|
||||||
|
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, menge);
|
||||||
confirm.style.display = "block";
|
confirm.style.display = "block";
|
||||||
setTimeout(() => (confirm.style.display = "none"), 2500);
|
setTimeout(() => (confirm.style.display = "none"), 2500);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
||||||
|
|||||||
@@ -166,7 +166,12 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
const item = getCart().find((i) => i.slug === slug);
|
const item = getCart().find((i) => i.slug === slug);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (action === "inc") updateQuantity(slug, item.menge + 1);
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist.
|
||||||
|
if (action === "inc") {
|
||||||
|
const produkt = getProduct(slug);
|
||||||
|
const max = produkt ? produkt.bestand : Infinity;
|
||||||
|
updateQuantity(slug, Math.min(item.menge + 1, max));
|
||||||
|
}
|
||||||
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
||||||
if (action === "remove") removeFromCart(slug);
|
if (action === "remove") removeFromCart(slug);
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const felder: [string, string | undefined][] = [
|
|||||||
<label for="qty">{t.common.quantity}</label>
|
<label for="qty">{t.common.quantity}</label>
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button type="button" id="qty-dec" aria-label="−">−</button>
|
<button type="button" id="qty-dec" aria-label="−">−</button>
|
||||||
<input type="number" id="qty" min="1" value="1" inputmode="numeric" />
|
<input type="number" id="qty" min="1" max={product.bestand} value={product.bestand > 0 ? 1 : 0} inputmode="numeric" data-bestand={product.bestand} />
|
||||||
<button type="button" id="qty-inc" aria-label="+">+</button>
|
<button type="button" id="qty-inc" aria-label="+">+</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -99,10 +99,12 @@ const felder: [string, string | undefined][] = [
|
|||||||
data-slug={product.slug}
|
data-slug={product.slug}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-preis={finalPrice}
|
data-preis={finalPrice}
|
||||||
|
data-bestand={product.bestand}
|
||||||
>
|
>
|
||||||
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="small" id="stock-hint" style="display:none;"></p>
|
||||||
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
||||||
|
|
||||||
{felder.some(([, v]) => v) && (
|
{felder.some(([, v]) => v) && (
|
||||||
@@ -131,22 +133,78 @@ const felder: [string, string | undefined][] = [
|
|||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script define:vars={{ onlyLeftTemplate: t.common.onlyXLeft("__N__"), maxInCartText: t.common.maxInCart, soldOutText: t.common.soldOut, addToCartText: t.common.addToCart }}>
|
||||||
|
window.__productStockVars = { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText };
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import { addToCart } from "../../../scripts/cart";
|
import { addToCart, getCart } from "../../../scripts/cart";
|
||||||
|
const { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText } = (window as any).__productStockVars;
|
||||||
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
||||||
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
||||||
const confirm = document.getElementById("add-confirm")!;
|
const confirm = document.getElementById("add-confirm")!;
|
||||||
|
const stockHint = document.getElementById("stock-hint")!;
|
||||||
|
const bestand = Number(qtyInput.dataset.bestand || 0);
|
||||||
|
const slug = btn?.dataset.slug;
|
||||||
|
|
||||||
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist — zählt dabei mit,
|
||||||
|
// was von diesem Produkt schon im Warenkorb liegt (nicht nur die aktuelle Eingabe hier).
|
||||||
|
function bereitsImWarenkorb() {
|
||||||
|
return getCart().find((i) => i.slug === slug)?.menge ?? 0;
|
||||||
|
}
|
||||||
|
function maxJetztHinzufuegbar() {
|
||||||
|
return Math.max(0, bestand - bereitsImWarenkorb());
|
||||||
|
}
|
||||||
|
function clamp() {
|
||||||
|
const max = maxJetztHinzufuegbar();
|
||||||
|
let val = Math.max(0, Math.floor(Number(qtyInput.value) || 0));
|
||||||
|
if (val > max) val = max;
|
||||||
|
if (val < 1 && max > 0) val = 1;
|
||||||
|
qtyInput.value = String(val);
|
||||||
|
qtyInput.max = String(max);
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
if (bestand === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = soldOutText;
|
||||||
|
} else if (max === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = maxInCartText;
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = maxInCartText;
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = addToCartText;
|
||||||
|
if (max <= 5) {
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = onlyLeftTemplate.replace("__N__", String(max));
|
||||||
|
} else {
|
||||||
|
stockHint.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
clamp();
|
||||||
|
|
||||||
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Number(qtyInput.value) + 1);
|
qtyInput.value = String(Number(qtyInput.value) + 1);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
qtyInput.addEventListener("input", clamp);
|
||||||
|
qtyInput.addEventListener("change", clamp);
|
||||||
|
|
||||||
btn?.addEventListener("click", () => {
|
btn?.addEventListener("click", () => {
|
||||||
const { slug, name, preis } = btn.dataset;
|
const menge = clamp();
|
||||||
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
|
if (menge < 1) return;
|
||||||
|
const { name, preis } = btn.dataset;
|
||||||
|
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, menge);
|
||||||
confirm.style.display = "block";
|
confirm.style.display = "block";
|
||||||
setTimeout(() => (confirm.style.display = "none"), 2500);
|
setTimeout(() => (confirm.style.display = "none"), 2500);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
||||||
|
|||||||
@@ -166,7 +166,12 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
const item = getCart().find((i) => i.slug === slug);
|
const item = getCart().find((i) => i.slug === slug);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (action === "inc") updateQuantity(slug, item.menge + 1);
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist.
|
||||||
|
if (action === "inc") {
|
||||||
|
const produkt = getProduct(slug);
|
||||||
|
const max = produkt ? produkt.bestand : Infinity;
|
||||||
|
updateQuantity(slug, Math.min(item.menge + 1, max));
|
||||||
|
}
|
||||||
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
||||||
if (action === "remove") removeFromCart(slug);
|
if (action === "remove") removeFromCart(slug);
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const felder: [string, string | undefined][] = [
|
|||||||
<label for="qty">{t.common.quantity}</label>
|
<label for="qty">{t.common.quantity}</label>
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button type="button" id="qty-dec" aria-label="−">−</button>
|
<button type="button" id="qty-dec" aria-label="−">−</button>
|
||||||
<input type="number" id="qty" min="1" value="1" inputmode="numeric" />
|
<input type="number" id="qty" min="1" max={product.bestand} value={product.bestand > 0 ? 1 : 0} inputmode="numeric" data-bestand={product.bestand} />
|
||||||
<button type="button" id="qty-inc" aria-label="+">+</button>
|
<button type="button" id="qty-inc" aria-label="+">+</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -99,10 +99,12 @@ const felder: [string, string | undefined][] = [
|
|||||||
data-slug={product.slug}
|
data-slug={product.slug}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-preis={finalPrice}
|
data-preis={finalPrice}
|
||||||
|
data-bestand={product.bestand}
|
||||||
>
|
>
|
||||||
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="small" id="stock-hint" style="display:none;"></p>
|
||||||
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
||||||
|
|
||||||
{felder.some(([, v]) => v) && (
|
{felder.some(([, v]) => v) && (
|
||||||
@@ -131,22 +133,78 @@ const felder: [string, string | undefined][] = [
|
|||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script define:vars={{ onlyLeftTemplate: t.common.onlyXLeft("__N__"), maxInCartText: t.common.maxInCart, soldOutText: t.common.soldOut, addToCartText: t.common.addToCart }}>
|
||||||
|
window.__productStockVars = { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText };
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import { addToCart } from "../../../scripts/cart";
|
import { addToCart, getCart } from "../../../scripts/cart";
|
||||||
|
const { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText } = (window as any).__productStockVars;
|
||||||
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
||||||
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
||||||
const confirm = document.getElementById("add-confirm")!;
|
const confirm = document.getElementById("add-confirm")!;
|
||||||
|
const stockHint = document.getElementById("stock-hint")!;
|
||||||
|
const bestand = Number(qtyInput.dataset.bestand || 0);
|
||||||
|
const slug = btn?.dataset.slug;
|
||||||
|
|
||||||
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist — zählt dabei mit,
|
||||||
|
// was von diesem Produkt schon im Warenkorb liegt (nicht nur die aktuelle Eingabe hier).
|
||||||
|
function bereitsImWarenkorb() {
|
||||||
|
return getCart().find((i) => i.slug === slug)?.menge ?? 0;
|
||||||
|
}
|
||||||
|
function maxJetztHinzufuegbar() {
|
||||||
|
return Math.max(0, bestand - bereitsImWarenkorb());
|
||||||
|
}
|
||||||
|
function clamp() {
|
||||||
|
const max = maxJetztHinzufuegbar();
|
||||||
|
let val = Math.max(0, Math.floor(Number(qtyInput.value) || 0));
|
||||||
|
if (val > max) val = max;
|
||||||
|
if (val < 1 && max > 0) val = 1;
|
||||||
|
qtyInput.value = String(val);
|
||||||
|
qtyInput.max = String(max);
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
if (bestand === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = soldOutText;
|
||||||
|
} else if (max === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = maxInCartText;
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = maxInCartText;
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = addToCartText;
|
||||||
|
if (max <= 5) {
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = onlyLeftTemplate.replace("__N__", String(max));
|
||||||
|
} else {
|
||||||
|
stockHint.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
clamp();
|
||||||
|
|
||||||
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Number(qtyInput.value) + 1);
|
qtyInput.value = String(Number(qtyInput.value) + 1);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
qtyInput.addEventListener("input", clamp);
|
||||||
|
qtyInput.addEventListener("change", clamp);
|
||||||
|
|
||||||
btn?.addEventListener("click", () => {
|
btn?.addEventListener("click", () => {
|
||||||
const { slug, name, preis } = btn.dataset;
|
const menge = clamp();
|
||||||
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
|
if (menge < 1) return;
|
||||||
|
const { name, preis } = btn.dataset;
|
||||||
|
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, menge);
|
||||||
confirm.style.display = "block";
|
confirm.style.display = "block";
|
||||||
setTimeout(() => (confirm.style.display = "none"), 2500);
|
setTimeout(() => (confirm.style.display = "none"), 2500);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
||||||
|
|||||||
@@ -166,7 +166,12 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../../data/rabatt
|
|||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
const item = getCart().find((i) => i.slug === slug);
|
const item = getCart().find((i) => i.slug === slug);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (action === "inc") updateQuantity(slug, item.menge + 1);
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist.
|
||||||
|
if (action === "inc") {
|
||||||
|
const produkt = getProduct(slug);
|
||||||
|
const max = produkt ? produkt.bestand : Infinity;
|
||||||
|
updateQuantity(slug, Math.min(item.menge + 1, max));
|
||||||
|
}
|
||||||
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
||||||
if (action === "remove") removeFromCart(slug);
|
if (action === "remove") removeFromCart(slug);
|
||||||
render();
|
render();
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const felder: [string, string | undefined][] = [
|
|||||||
<label for="qty">{t.common.quantity}</label>
|
<label for="qty">{t.common.quantity}</label>
|
||||||
<div class="qty-stepper">
|
<div class="qty-stepper">
|
||||||
<button type="button" id="qty-dec" aria-label="−">−</button>
|
<button type="button" id="qty-dec" aria-label="−">−</button>
|
||||||
<input type="number" id="qty" min="1" value="1" inputmode="numeric" />
|
<input type="number" id="qty" min="1" max={product.bestand} value={product.bestand > 0 ? 1 : 0} inputmode="numeric" data-bestand={product.bestand} />
|
||||||
<button type="button" id="qty-inc" aria-label="+">+</button>
|
<button type="button" id="qty-inc" aria-label="+">+</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -99,10 +99,12 @@ const felder: [string, string | undefined][] = [
|
|||||||
data-slug={product.slug}
|
data-slug={product.slug}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-preis={finalPrice}
|
data-preis={finalPrice}
|
||||||
|
data-bestand={product.bestand}
|
||||||
>
|
>
|
||||||
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="small" id="stock-hint" style="display:none;"></p>
|
||||||
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
|
||||||
|
|
||||||
{felder.some(([, v]) => v) && (
|
{felder.some(([, v]) => v) && (
|
||||||
@@ -131,22 +133,78 @@ const felder: [string, string | undefined][] = [
|
|||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script define:vars={{ onlyLeftTemplate: t.common.onlyXLeft("__N__"), maxInCartText: t.common.maxInCart, soldOutText: t.common.soldOut, addToCartText: t.common.addToCart }}>
|
||||||
|
window.__productStockVars = { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText };
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
import { addToCart } from "../../scripts/cart";
|
import { addToCart, getCart } from "../../scripts/cart";
|
||||||
|
const { onlyLeftTemplate, maxInCartText, soldOutText, addToCartText } = (window as any).__productStockVars;
|
||||||
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
|
||||||
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
const qtyInput = document.getElementById("qty") as HTMLInputElement;
|
||||||
const confirm = document.getElementById("add-confirm")!;
|
const confirm = document.getElementById("add-confirm")!;
|
||||||
|
const stockHint = document.getElementById("stock-hint")!;
|
||||||
|
const bestand = Number(qtyInput.dataset.bestand || 0);
|
||||||
|
const slug = btn?.dataset.slug;
|
||||||
|
|
||||||
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist — zählt dabei mit,
|
||||||
|
// was von diesem Produkt schon im Warenkorb liegt (nicht nur die aktuelle Eingabe hier).
|
||||||
|
function bereitsImWarenkorb() {
|
||||||
|
return getCart().find((i) => i.slug === slug)?.menge ?? 0;
|
||||||
|
}
|
||||||
|
function maxJetztHinzufuegbar() {
|
||||||
|
return Math.max(0, bestand - bereitsImWarenkorb());
|
||||||
|
}
|
||||||
|
function clamp() {
|
||||||
|
const max = maxJetztHinzufuegbar();
|
||||||
|
let val = Math.max(0, Math.floor(Number(qtyInput.value) || 0));
|
||||||
|
if (val > max) val = max;
|
||||||
|
if (val < 1 && max > 0) val = 1;
|
||||||
|
qtyInput.value = String(val);
|
||||||
|
qtyInput.max = String(max);
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
if (bestand === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = soldOutText;
|
||||||
|
} else if (max === 0) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = maxInCartText;
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = maxInCartText;
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = addToCartText;
|
||||||
|
if (max <= 5) {
|
||||||
|
stockHint.style.display = "block";
|
||||||
|
stockHint.textContent = onlyLeftTemplate.replace("__N__", String(max));
|
||||||
|
} else {
|
||||||
|
stockHint.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
clamp();
|
||||||
|
|
||||||
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
document.getElementById("qty-dec")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
qtyInput.value = String(Math.max(1, Number(qtyInput.value) - 1));
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
document.getElementById("qty-inc")?.addEventListener("click", () => {
|
||||||
qtyInput.value = String(Number(qtyInput.value) + 1);
|
qtyInput.value = String(Number(qtyInput.value) + 1);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
qtyInput.addEventListener("input", clamp);
|
||||||
|
qtyInput.addEventListener("change", clamp);
|
||||||
|
|
||||||
btn?.addEventListener("click", () => {
|
btn?.addEventListener("click", () => {
|
||||||
const { slug, name, preis } = btn.dataset;
|
const menge = clamp();
|
||||||
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
|
if (menge < 1) return;
|
||||||
|
const { name, preis } = btn.dataset;
|
||||||
|
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, menge);
|
||||||
confirm.style.display = "block";
|
confirm.style.display = "block";
|
||||||
setTimeout(() => (confirm.style.display = "none"), 2500);
|
setTimeout(() => (confirm.style.display = "none"), 2500);
|
||||||
|
clamp();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
// Klick auf ein Miniaturbild zeigt es groß im Hauptbild an (mehrere Produktfotos).
|
||||||
|
|||||||
@@ -166,7 +166,12 @@ import { effektiverZeilenpreis, mengenrabattProzent } from "../../data/rabatt";
|
|||||||
const action = target.dataset.action;
|
const action = target.dataset.action;
|
||||||
const item = getCart().find((i) => i.slug === slug);
|
const item = getCart().find((i) => i.slug === slug);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (action === "inc") updateQuantity(slug, item.menge + 1);
|
// Nie mehr in den Warenkorb legen können, als tatsächlich auf Lager ist.
|
||||||
|
if (action === "inc") {
|
||||||
|
const produkt = getProduct(slug);
|
||||||
|
const max = produkt ? produkt.bestand : Infinity;
|
||||||
|
updateQuantity(slug, Math.min(item.menge + 1, max));
|
||||||
|
}
|
||||||
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
if (action === "dec") updateQuantity(slug, item.menge - 1);
|
||||||
if (action === "remove") removeFromCart(slug);
|
if (action === "remove") removeFromCart(slug);
|
||||||
render();
|
render();
|
||||||
|
|||||||
+12
-2
@@ -38,19 +38,29 @@ export function saveCart(items: CartItem[]) {
|
|||||||
window.dispatchEvent(new CustomEvent("cart:changed", { detail: items }));
|
window.dispatchEvent(new CustomEvent("cart:changed", { detail: items }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Nie mehr als tatsächlich auf Lager ist — greift unabhängig davon, welche Oberfläche
|
||||||
|
* addToCart()/updateQuantity() aufruft, damit diese Regel garantiert eingehalten wird und
|
||||||
|
* nicht nur an einer einzelnen Stelle in der UI. */
|
||||||
|
function maxBestand(slug: string): number {
|
||||||
|
const produkt = getProduct(slug);
|
||||||
|
return produkt ? produkt.bestand : Infinity;
|
||||||
|
}
|
||||||
|
|
||||||
export function addToCart(item: Omit<CartItem, "menge">, menge = 1) {
|
export function addToCart(item: Omit<CartItem, "menge">, menge = 1) {
|
||||||
const cart = getCart();
|
const cart = getCart();
|
||||||
|
const max = maxBestand(item.slug);
|
||||||
const existing = cart.find((i) => i.slug === item.slug);
|
const existing = cart.find((i) => i.slug === item.slug);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.menge += menge;
|
existing.menge = Math.min(existing.menge + menge, max);
|
||||||
} else {
|
} else {
|
||||||
cart.push({ ...item, menge });
|
cart.push({ ...item, menge: Math.min(Math.max(1, menge), max) });
|
||||||
}
|
}
|
||||||
saveCart(cart);
|
saveCart(cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateQuantity(slug: string, menge: number) {
|
export function updateQuantity(slug: string, menge: number) {
|
||||||
let cart = getCart();
|
let cart = getCart();
|
||||||
|
menge = Math.min(menge, maxBestand(slug));
|
||||||
if (menge <= 0) {
|
if (menge <= 0) {
|
||||||
cart = cart.filter((i) => i.slug !== slug);
|
cart = cart.filter((i) => i.slug !== slug);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user