Rabatt-System, größerer Mengen-Stepper, Zahlungsart zuerst, echte HasiDog-Inhalte
- Neues Rabatt-System: Prozent-/Betrag-Rabatt mit optionalem Ablaufdatum, Mengenrabatt-Staffel pro Produkt, sowie site-weite Gutscheincodes. Wird live im Warenkorb/Checkout berechnet (src/data/rabatt.ts, src/data/gutscheine.ts, cart.ts) und ist im Adminbereich direkt unter dem Preis bearbeitbar (config.yml: "Rabatte" + neue Collection "Gutscheincodes"). Effektiver Preis + Rabatt-Badge auf Produktkarten und Produktseite sichtbar. - Mengen-Stepper auf der Produktseite vergrößert (bereits vorbereitet, jetzt mit Build/Test verifiziert). - Zahlungsart im Checkout auf Platz 1, mit markenfarbigen, fertig gestalteten Buttons (PayPal/Klarna/Banküberweisung). - Unterkategorie-Seiten zeigen jetzt VanVans eigene Produktbeschreibung und das echte Fotos des Produkts statt Platzhaltertext, mit kleinem Link zur HasiDog-Universum-Seite statt großem Button. Verifiziert per Puppeteer-Funktionstest gegen den echten Production-Build (Rabatt-Berechnung, Mengenrabatt, Gutschein-Einlösung, Checkout-Summary, keine Konsolenfehler).
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
// Gutscheincodes — site-weit gültig, von VanVan im Adminbereich unter "🎟️ Gutscheincodes"
|
||||
// verwaltet. Werden im Warenkorb/Checkout eingelöst und auf den Gesamt-Warenwert angewendet.
|
||||
|
||||
// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst
|
||||
import gutscheineRaw from "../content/gutscheine.json";
|
||||
|
||||
export interface Gutschein {
|
||||
code: string;
|
||||
prozent?: number;
|
||||
betrag?: number;
|
||||
bis?: string; // ISO-Datum, optional
|
||||
}
|
||||
|
||||
const gutscheine = (gutscheineRaw as { codes: Gutschein[] }).codes ?? [];
|
||||
|
||||
function istGueltig(bis?: string): boolean {
|
||||
if (!bis) return true;
|
||||
return new Date() <= new Date(`${bis}T23:59:59`);
|
||||
}
|
||||
|
||||
/** Sucht einen Gutschein anhand des eingegebenen Codes (Groß-/Kleinschreibung egal). */
|
||||
export function findeGutschein(code: string): Gutschein | undefined {
|
||||
const normalisiert = code.trim().toUpperCase();
|
||||
return gutscheine.find((g) => g.code.toUpperCase() === normalisiert && istGueltig(g.bis));
|
||||
}
|
||||
|
||||
/** Berechnet den Rabatt-Betrag in Euro für einen gegebenen Warenwert. */
|
||||
export function gutscheinRabatt(gutschein: Gutschein, warenwert: number): number {
|
||||
if (typeof gutschein.prozent === "number" && gutschein.prozent > 0) {
|
||||
return Math.min(warenwert, warenwert * (gutschein.prozent / 100));
|
||||
}
|
||||
if (typeof gutschein.betrag === "number" && gutschein.betrag > 0) {
|
||||
return Math.min(warenwert, gutschein.betrag);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -34,6 +34,15 @@ export interface Product {
|
||||
// Versandkosten-Staffel (siehe src/data/versand.ts). Optional, damit Bestandsprodukte ohne
|
||||
// diese Angabe nicht kaputtgehen — dann greift automatisch die Staffel als Rückfalllösung.
|
||||
versand?: { de?: number; at?: number; ch?: number; lu?: number };
|
||||
// Rabatte — mehrere Arten gleichzeitig möglich (siehe src/data/rabatt.ts für die Logik):
|
||||
// 1. Prozent/Betrag-Rabatt, optional zeitlich befristet (läuft automatisch ab).
|
||||
// 2. Mengenrabatt-Staffel (ab X Stück Y% Rabatt), wird im Warenkorb automatisch angewendet.
|
||||
rabatt?: {
|
||||
prozent?: number; // z.B. 20 = 20% Rabatt auf den Preis
|
||||
betrag?: number; // Alternative zu prozent: fester Euro-Betrag Rabatt
|
||||
bis?: string; // ISO-Datum, z.B. "2026-12-31" — Rabatt gilt nur bis einschließlich diesem Tag
|
||||
mengenrabatt?: { abMenge: number; prozent: number }[];
|
||||
};
|
||||
}
|
||||
|
||||
const modules = import.meta.glob("/src/content/products/*.json", { eager: true }) as Record<
|
||||
@@ -53,6 +62,10 @@ export function productsByCategory(kategorie: string): Product[] {
|
||||
return products.filter((p) => p.kategorie === kategorie);
|
||||
}
|
||||
|
||||
export function productsByUnterkategorie(kategorie: string, unterkategorie: string): Product[] {
|
||||
return products.filter((p) => p.kategorie === kategorie && p.unterkategorie === unterkategorie);
|
||||
}
|
||||
|
||||
export function saleProducts(): Product[] {
|
||||
return products.filter((p) => p.badges.includes("sale") || p.preisAlt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Rabatt-Logik. Unterstützt "alle Art von Rabatten" (Wunsch vom 02.08.2026):
|
||||
// 1. Klassischer Sale-Preis (bereits vorhanden: `preis`/`preisAlt`, manuell gesetzt).
|
||||
// 2. Prozentualer oder fester Euro-Rabatt am Produkt, automatisch berechnet, optional
|
||||
// zeitlich befristet (läuft am Stichtag von selbst wieder ab, ohne dass VanVan etwas
|
||||
// manuell zurückstellen muss).
|
||||
// 3. Mengenrabatt-Staffel (z.B. "ab 3 Stück 10% Rabatt") — wird im Warenkorb automatisch
|
||||
// auf die Zeile angewendet.
|
||||
// 4. Gutscheincodes (site-weit, siehe src/data/gutscheine.ts) — im Warenkorb/Checkout einlösbar.
|
||||
|
||||
import type { Product } from "./products";
|
||||
|
||||
/** Prüft, ob ein befristeter Rabatt heute noch gültig ist (Vergleich läuft im Browser zur
|
||||
* echten aktuellen Uhrzeit, nicht nur zur Build-Zeit — der Rabatt läuft also wirklich
|
||||
* automatisch ab, auch ohne neuen Deploy). */
|
||||
export function istRabattAktiv(bis?: string): boolean {
|
||||
if (!bis) return true;
|
||||
const heute = new Date();
|
||||
const ende = new Date(`${bis}T23:59:59`);
|
||||
return heute <= ende;
|
||||
}
|
||||
|
||||
/** Berechnet den effektiven Verkaufspreis eines Produkts unter Berücksichtigung von
|
||||
* Prozent-/Betrag-Rabatt (falls aktiv). Reiner Sale-Preis über `preisAlt` bleibt unabhängig
|
||||
* davon bestehen und wird hier NICHT verändert — beide Mechanismen sind unabhängig
|
||||
* kombinierbar (z.B. Sale-Preis als Basis, zusätzlich zeitlich befristete Rabattaktion oben drauf). */
|
||||
export function effektiverPreis(product: Product): number {
|
||||
const basis = product.preis;
|
||||
const r = product.rabatt;
|
||||
if (!r || !istRabattAktiv(r.bis)) return basis;
|
||||
if (typeof r.prozent === "number" && r.prozent > 0) {
|
||||
return Math.max(0, basis * (1 - r.prozent / 100));
|
||||
}
|
||||
if (typeof r.betrag === "number" && r.betrag > 0) {
|
||||
return Math.max(0, basis - r.betrag);
|
||||
}
|
||||
return basis;
|
||||
}
|
||||
|
||||
/** Gibt an, ob gerade ein aktiver Rabatt greift (für Badge-Anzeige "-20%" o.ä.). */
|
||||
export function hatAktivenRabatt(product: Product): boolean {
|
||||
return effektiverPreis(product) < product.preis;
|
||||
}
|
||||
|
||||
/** Für die Badge-Anzeige: "-20%" oder "-5 €", je nachdem was hinterlegt ist. */
|
||||
export function rabattLabel(product: Product): string | null {
|
||||
const r = product.rabatt;
|
||||
if (!r || !istRabattAktiv(r.bis)) return null;
|
||||
if (typeof r.prozent === "number" && r.prozent > 0) return `-${r.prozent}%`;
|
||||
if (typeof r.betrag === "number" && r.betrag > 0) return `-${r.betrag.toFixed(2).replace(".", ",")} €`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Mengenrabatt: findet den besten (höchsten) Prozentsatz, der bei der gegebenen Stückzahl
|
||||
* greift. Gibt 0 zurück, wenn kein Mengenrabatt hinterlegt ist oder die Menge zu klein ist. */
|
||||
export function mengenrabattProzent(product: Product, menge: number): number {
|
||||
const stufen = product.rabatt?.mengenrabatt ?? [];
|
||||
let bester = 0;
|
||||
for (const stufe of stufen) {
|
||||
if (menge >= stufe.abMenge && stufe.prozent > bester) bester = stufe.prozent;
|
||||
}
|
||||
return bester;
|
||||
}
|
||||
|
||||
/** Effektiver Zeilenpreis (Einzelpreis inkl. Rabatt × Menge, zusätzlich mit Mengenrabatt
|
||||
* verrechnet) — das ist die Zahl, die im Warenkorb pro Position angezeigt/summiert wird. */
|
||||
export function effektiverZeilenpreis(product: Product, menge: number): number {
|
||||
const einzelpreis = effektiverPreis(product);
|
||||
const mengenProzent = mengenrabattProzent(product, menge);
|
||||
const zeile = einzelpreis * menge;
|
||||
return mengenProzent > 0 ? zeile * (1 - mengenProzent / 100) : zeile;
|
||||
}
|
||||
Reference in New Issue
Block a user