Neu: Kundenrezensionen mit Herzen in Lila-Babyblau, ueberall im Shop integriert
Neue HeartRating-Komponente (Herzen statt Sterne, Lila-Babyblau-Verlauf wie der Rest der Markenoptik) + komplettes Rezensions-System: - Produktkarten: Bewertungszeile (Herzen + Anzahl) unter dem Titel, nur wenn Bewertungen vorhanden sind. - Produktseite: volle "Kundenrezensionen"-Sektion mit Durchschnitt und einzelnen Rezensionen (Name, Herzen, Datum, Text) bzw. "Noch keine Bewertungen"-Hinweis. - Shop-Filter: neuer "Mindestbewertung"-Filter im Filter-Sidebar + neue Sortierung "Beste Bewertung". - Startseite: "Was unsere Kund:innen sagen"-Sektion mit Gesamtbewertung und den 3 neuesten Rezensionen - erscheint automatisch, sobald die erste echte Bewertung existiert. - Adminbereich: neues Feld "Bewertungen" pro Produkt (Name/Herzen/Text/Datum), mit deutlichem Hinweis, dass hier nur echte Kundenrueckmeldungen eingetragen werden duerfen - erfundene Bewertungen sind in Deutschland/der EU gesetzlich verboten (UWG). Deshalb bewusst OHNE Beispiel-Bewertungen im Code: alle Bereiche starten leer und blenden sich automatisch ein, sobald VanVan die erste echte Rezension eintraegt. Mit temporaeren Testdaten end-to-end durchgetestet (Herzen-Darstellung, Rezensions-Section, Filter, Startseiten-Sektion) und vor dem Commit wieder aus den echten Produktdaten entfernt. Gilt fuer alle 4 Sprachen. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1add9a42a1
commit
b384938b92
@@ -1,15 +1,20 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import HeartRating from "../../components/HeartRating.astro";
|
||||
import { categories } from "../../data/categories";
|
||||
import { products } from "../../data/products";
|
||||
import { products, alleBewertungen, gesamtDurchschnitt } from "../../data/products";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
|
||||
const lang: Locale = "ch";
|
||||
const t = useTranslations(lang);
|
||||
|
||||
const neuheiten = products.filter((p) => p.badges.includes("neu")).slice(0, 4);
|
||||
const bestseller = products.filter((p) => p.badges.includes("bestseller")).slice(0, 4);
|
||||
const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).slice(0, 4);
|
||||
const topBewertungen = alleBewertungen().slice(0, 3);
|
||||
const bewertungSchnittGesamt = gesamtDurchschnitt();
|
||||
---
|
||||
<Layout title="Startsyte" lang={lang} path="/">
|
||||
<section class="hero">
|
||||
@@ -84,6 +89,31 @@ const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).sli
|
||||
</section>
|
||||
)}
|
||||
|
||||
{topBewertungen.length > 0 && bewertungSchnittGesamt !== null && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">🩵 {t.reviews.heading}</span>
|
||||
<h2>{t.reviews.heading}</h2>
|
||||
<p class="lead">{t.reviews.lead}</p>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnittGesamt} size={24} showValue={true} anzahlLabel={t.reviews.basedOn(alleBewertungen().length)} />
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
{topBewertungen.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
<a class="small" href={`/ch/produkt/${r.produktSlug}/`}>{t.reviews.toProduct} {r.produktName[lang]}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="section">
|
||||
<div class="container grid grid-2 creator-section">
|
||||
<div class="portrait-frame creator-photo">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { products, getProduct, productsByCategory } from "../../../data/products";
|
||||
import HeartRating from "../../../components/HeartRating.astro";
|
||||
import { products, getProduct, productsByCategory, durchschnittsbewertung, anzahlBewertungen } from "../../../data/products";
|
||||
import { getCategory } from "../../../data/categories";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
@@ -27,6 +28,8 @@ const finalPrice = effektiverPreis(product);
|
||||
const discountBadge = rabattLabel(product);
|
||||
const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product);
|
||||
const mengenrabatt = product.rabatt?.mengenrabatt ?? [];
|
||||
const bewertungSchnitt = durchschnittsbewertung(product);
|
||||
const bewertungAnzahl = anzahlBewertungen(product);
|
||||
|
||||
const felder: [string, string | undefined][] = [
|
||||
[t.product.articleNo, product.artikelnummer],
|
||||
@@ -71,6 +74,11 @@ const felder: [string, string | undefined][] = [
|
||||
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
|
||||
</div>
|
||||
<h1>{name}</h1>
|
||||
{bewertungSchnitt !== null && (
|
||||
<a href="#bewertungen" class="review-summary-link">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={16} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</a>
|
||||
)}
|
||||
<div class="price-row-detail">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
{showOldAsPreis && <span class="price-old">{formatPrice(product.preis, lang)}</span>}
|
||||
@@ -123,6 +131,33 @@ const felder: [string, string | undefined][] = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight" id="bewertungen">
|
||||
<div class="container">
|
||||
<h2>{t.product.reviewsTitle}</h2>
|
||||
{bewertungSchnitt !== null ? (
|
||||
<>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={26} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</div>
|
||||
<div class="review-list">
|
||||
{product.bewertungen!.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
{r.datum && <span class="review-date small">{r.datum}</span>}
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p class="small">{t.product.noReviews}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{empfehlungen.length > 0 && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { categories, subcategoryChipLabel } from "../../../data/categories";
|
||||
import { products } from "../../../data/products";
|
||||
import { products, durchschnittsbewertung } from "../../../data/products";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
|
||||
@@ -59,6 +59,16 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-bewertung">{t.shop.minRatingLabel}</label>
|
||||
<select id="f-bewertung">
|
||||
<option value="0">{t.shop.minRatingAny}</option>
|
||||
<option value="4">🩵🩵🩵🩵 +</option>
|
||||
<option value="3">🩵🩵🩵 +</option>
|
||||
<option value="2">🩵🩵 +</option>
|
||||
<option value="1">🩵 +</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="small">{t.shop.filterNote}</p>
|
||||
</aside>
|
||||
|
||||
@@ -68,6 +78,7 @@ const t = useTranslations(lang);
|
||||
<select id="sort" aria-label={t.shop.sortLabel}>
|
||||
<option value="neu">{t.shop.sortNew}</option>
|
||||
<option value="bestseller">{t.shop.sortBestseller}</option>
|
||||
<option value="bewertung">{t.shop.sortRating}</option>
|
||||
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
|
||||
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
|
||||
</select>
|
||||
@@ -82,6 +93,7 @@ const t = useTranslations(lang);
|
||||
data-bestand={p.bestand}
|
||||
data-neu={p.badges.includes("neu") ? 1 : 0}
|
||||
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
|
||||
data-bewertung={durchschnittsbewertung(p) ?? 0}
|
||||
>
|
||||
<ProductCard product={p} lang={lang} />
|
||||
</div>
|
||||
@@ -101,6 +113,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
const emptyMsg = document.getElementById("empty-msg");
|
||||
@@ -114,6 +127,7 @@ const t = useTranslations(lang);
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
let visible = 0;
|
||||
@@ -125,7 +139,8 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar;
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -138,13 +153,14 @@ const t = useTranslations(lang);
|
||||
case "preis-auf": return Number(a.dataset.preis) - Number(b.dataset.preis);
|
||||
case "preis-ab": return Number(b.dataset.preis) - Number(a.dataset.preis);
|
||||
case "bestseller": return Number(b.dataset.bestseller) - Number(a.dataset.bestseller);
|
||||
case "bewertung": return Number(b.dataset.bewertung) - Number(a.dataset.bewertung);
|
||||
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
|
||||
}
|
||||
});
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bewertungSel, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
const catFilter = document.getElementById("cat-filter");
|
||||
const catFilterBtn = document.getElementById("cat-filter-btn");
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import HeartRating from "../../components/HeartRating.astro";
|
||||
import { categories } from "../../data/categories";
|
||||
import { products } from "../../data/products";
|
||||
import { products, alleBewertungen, gesamtDurchschnitt } from "../../data/products";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
|
||||
const lang: Locale = "en";
|
||||
const t = useTranslations(lang);
|
||||
|
||||
const neuheiten = products.filter((p) => p.badges.includes("neu")).slice(0, 4);
|
||||
const bestseller = products.filter((p) => p.badges.includes("bestseller")).slice(0, 4);
|
||||
const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).slice(0, 4);
|
||||
const topBewertungen = alleBewertungen().slice(0, 3);
|
||||
const bewertungSchnittGesamt = gesamtDurchschnitt();
|
||||
---
|
||||
<Layout title="Home" lang={lang} path="/">
|
||||
<section class="hero">
|
||||
@@ -84,6 +89,31 @@ const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).sli
|
||||
</section>
|
||||
)}
|
||||
|
||||
{topBewertungen.length > 0 && bewertungSchnittGesamt !== null && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">🩵 {t.reviews.heading}</span>
|
||||
<h2>{t.reviews.heading}</h2>
|
||||
<p class="lead">{t.reviews.lead}</p>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnittGesamt} size={24} showValue={true} anzahlLabel={t.reviews.basedOn(alleBewertungen().length)} />
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
{topBewertungen.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
<a class="small" href={`/en/produkt/${r.produktSlug}/`}>{t.reviews.toProduct} {r.produktName[lang]}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="section">
|
||||
<div class="container grid grid-2 creator-section">
|
||||
<div class="portrait-frame creator-photo">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { products, getProduct, productsByCategory } from "../../../data/products";
|
||||
import HeartRating from "../../../components/HeartRating.astro";
|
||||
import { products, getProduct, productsByCategory, durchschnittsbewertung, anzahlBewertungen } from "../../../data/products";
|
||||
import { getCategory } from "../../../data/categories";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
@@ -27,6 +28,8 @@ const finalPrice = effektiverPreis(product);
|
||||
const discountBadge = rabattLabel(product);
|
||||
const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product);
|
||||
const mengenrabatt = product.rabatt?.mengenrabatt ?? [];
|
||||
const bewertungSchnitt = durchschnittsbewertung(product);
|
||||
const bewertungAnzahl = anzahlBewertungen(product);
|
||||
|
||||
const felder: [string, string | undefined][] = [
|
||||
[t.product.articleNo, product.artikelnummer],
|
||||
@@ -71,6 +74,11 @@ const felder: [string, string | undefined][] = [
|
||||
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
|
||||
</div>
|
||||
<h1>{name}</h1>
|
||||
{bewertungSchnitt !== null && (
|
||||
<a href="#bewertungen" class="review-summary-link">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={16} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</a>
|
||||
)}
|
||||
<div class="price-row-detail">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
{showOldAsPreis && <span class="price-old">{formatPrice(product.preis, lang)}</span>}
|
||||
@@ -123,6 +131,33 @@ const felder: [string, string | undefined][] = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight" id="bewertungen">
|
||||
<div class="container">
|
||||
<h2>{t.product.reviewsTitle}</h2>
|
||||
{bewertungSchnitt !== null ? (
|
||||
<>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={26} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</div>
|
||||
<div class="review-list">
|
||||
{product.bewertungen!.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
{r.datum && <span class="review-date small">{r.datum}</span>}
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p class="small">{t.product.noReviews}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{empfehlungen.length > 0 && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { categories, subcategoryChipLabel } from "../../../data/categories";
|
||||
import { products } from "../../../data/products";
|
||||
import { products, durchschnittsbewertung } from "../../../data/products";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
|
||||
@@ -59,6 +59,16 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-bewertung">{t.shop.minRatingLabel}</label>
|
||||
<select id="f-bewertung">
|
||||
<option value="0">{t.shop.minRatingAny}</option>
|
||||
<option value="4">🩵🩵🩵🩵 +</option>
|
||||
<option value="3">🩵🩵🩵 +</option>
|
||||
<option value="2">🩵🩵 +</option>
|
||||
<option value="1">🩵 +</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="small">{t.shop.filterNote}</p>
|
||||
</aside>
|
||||
|
||||
@@ -68,6 +78,7 @@ const t = useTranslations(lang);
|
||||
<select id="sort" aria-label={t.shop.sortLabel}>
|
||||
<option value="neu">{t.shop.sortNew}</option>
|
||||
<option value="bestseller">{t.shop.sortBestseller}</option>
|
||||
<option value="bewertung">{t.shop.sortRating}</option>
|
||||
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
|
||||
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
|
||||
</select>
|
||||
@@ -82,6 +93,7 @@ const t = useTranslations(lang);
|
||||
data-bestand={p.bestand}
|
||||
data-neu={p.badges.includes("neu") ? 1 : 0}
|
||||
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
|
||||
data-bewertung={durchschnittsbewertung(p) ?? 0}
|
||||
>
|
||||
<ProductCard product={p} lang={lang} />
|
||||
</div>
|
||||
@@ -101,6 +113,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
const emptyMsg = document.getElementById("empty-msg");
|
||||
@@ -114,6 +127,7 @@ const t = useTranslations(lang);
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
let visible = 0;
|
||||
@@ -125,7 +139,8 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar;
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -138,13 +153,14 @@ const t = useTranslations(lang);
|
||||
case "preis-auf": return Number(a.dataset.preis) - Number(b.dataset.preis);
|
||||
case "preis-ab": return Number(b.dataset.preis) - Number(a.dataset.preis);
|
||||
case "bestseller": return Number(b.dataset.bestseller) - Number(a.dataset.bestseller);
|
||||
case "bewertung": return Number(b.dataset.bewertung) - Number(a.dataset.bewertung);
|
||||
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
|
||||
}
|
||||
});
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bewertungSel, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
const catFilter = document.getElementById("cat-filter");
|
||||
const catFilterBtn = document.getElementById("cat-filter-btn");
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import HeartRating from "../../components/HeartRating.astro";
|
||||
import { categories } from "../../data/categories";
|
||||
import { products } from "../../data/products";
|
||||
import { products, alleBewertungen, gesamtDurchschnitt } from "../../data/products";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
|
||||
const lang: Locale = "fr";
|
||||
const t = useTranslations(lang);
|
||||
|
||||
const neuheiten = products.filter((p) => p.badges.includes("neu")).slice(0, 4);
|
||||
const bestseller = products.filter((p) => p.badges.includes("bestseller")).slice(0, 4);
|
||||
const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).slice(0, 4);
|
||||
const topBewertungen = alleBewertungen().slice(0, 3);
|
||||
const bewertungSchnittGesamt = gesamtDurchschnitt();
|
||||
---
|
||||
<Layout title="Accueil" lang={lang} path="/">
|
||||
<section class="hero">
|
||||
@@ -84,6 +89,31 @@ const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).sli
|
||||
</section>
|
||||
)}
|
||||
|
||||
{topBewertungen.length > 0 && bewertungSchnittGesamt !== null && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">🩵 {t.reviews.heading}</span>
|
||||
<h2>{t.reviews.heading}</h2>
|
||||
<p class="lead">{t.reviews.lead}</p>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnittGesamt} size={24} showValue={true} anzahlLabel={t.reviews.basedOn(alleBewertungen().length)} />
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
{topBewertungen.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
<a class="small" href={`/fr/produkt/${r.produktSlug}/`}>{t.reviews.toProduct} {r.produktName[lang]}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="section">
|
||||
<div class="container grid grid-2 creator-section">
|
||||
<div class="portrait-frame creator-photo">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { products, getProduct, productsByCategory } from "../../../data/products";
|
||||
import HeartRating from "../../../components/HeartRating.astro";
|
||||
import { products, getProduct, productsByCategory, durchschnittsbewertung, anzahlBewertungen } from "../../../data/products";
|
||||
import { getCategory } from "../../../data/categories";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
@@ -27,6 +28,8 @@ const finalPrice = effektiverPreis(product);
|
||||
const discountBadge = rabattLabel(product);
|
||||
const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product);
|
||||
const mengenrabatt = product.rabatt?.mengenrabatt ?? [];
|
||||
const bewertungSchnitt = durchschnittsbewertung(product);
|
||||
const bewertungAnzahl = anzahlBewertungen(product);
|
||||
|
||||
const felder: [string, string | undefined][] = [
|
||||
[t.product.articleNo, product.artikelnummer],
|
||||
@@ -71,6 +74,11 @@ const felder: [string, string | undefined][] = [
|
||||
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
|
||||
</div>
|
||||
<h1>{name}</h1>
|
||||
{bewertungSchnitt !== null && (
|
||||
<a href="#bewertungen" class="review-summary-link">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={16} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</a>
|
||||
)}
|
||||
<div class="price-row-detail">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
{showOldAsPreis && <span class="price-old">{formatPrice(product.preis, lang)}</span>}
|
||||
@@ -123,6 +131,33 @@ const felder: [string, string | undefined][] = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight" id="bewertungen">
|
||||
<div class="container">
|
||||
<h2>{t.product.reviewsTitle}</h2>
|
||||
{bewertungSchnitt !== null ? (
|
||||
<>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={26} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</div>
|
||||
<div class="review-list">
|
||||
{product.bewertungen!.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
{r.datum && <span class="review-date small">{r.datum}</span>}
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p class="small">{t.product.noReviews}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{empfehlungen.length > 0 && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import ProductCard from "../../../components/ProductCard.astro";
|
||||
import { categories, subcategoryChipLabel } from "../../../data/categories";
|
||||
import { products } from "../../../data/products";
|
||||
import { products, durchschnittsbewertung } from "../../../data/products";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
|
||||
@@ -59,6 +59,16 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-bewertung">{t.shop.minRatingLabel}</label>
|
||||
<select id="f-bewertung">
|
||||
<option value="0">{t.shop.minRatingAny}</option>
|
||||
<option value="4">🩵🩵🩵🩵 +</option>
|
||||
<option value="3">🩵🩵🩵 +</option>
|
||||
<option value="2">🩵🩵 +</option>
|
||||
<option value="1">🩵 +</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="small">{t.shop.filterNote}</p>
|
||||
</aside>
|
||||
|
||||
@@ -68,6 +78,7 @@ const t = useTranslations(lang);
|
||||
<select id="sort" aria-label={t.shop.sortLabel}>
|
||||
<option value="neu">{t.shop.sortNew}</option>
|
||||
<option value="bestseller">{t.shop.sortBestseller}</option>
|
||||
<option value="bewertung">{t.shop.sortRating}</option>
|
||||
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
|
||||
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
|
||||
</select>
|
||||
@@ -82,6 +93,7 @@ const t = useTranslations(lang);
|
||||
data-bestand={p.bestand}
|
||||
data-neu={p.badges.includes("neu") ? 1 : 0}
|
||||
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
|
||||
data-bewertung={durchschnittsbewertung(p) ?? 0}
|
||||
>
|
||||
<ProductCard product={p} lang={lang} />
|
||||
</div>
|
||||
@@ -101,6 +113,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
const emptyMsg = document.getElementById("empty-msg");
|
||||
@@ -114,6 +127,7 @@ const t = useTranslations(lang);
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
let visible = 0;
|
||||
@@ -125,7 +139,8 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar;
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -138,13 +153,14 @@ const t = useTranslations(lang);
|
||||
case "preis-auf": return Number(a.dataset.preis) - Number(b.dataset.preis);
|
||||
case "preis-ab": return Number(b.dataset.preis) - Number(a.dataset.preis);
|
||||
case "bestseller": return Number(b.dataset.bestseller) - Number(a.dataset.bestseller);
|
||||
case "bewertung": return Number(b.dataset.bewertung) - Number(a.dataset.bewertung);
|
||||
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
|
||||
}
|
||||
});
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bewertungSel, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
const catFilter = document.getElementById("cat-filter");
|
||||
const catFilterBtn = document.getElementById("cat-filter-btn");
|
||||
|
||||
+34
-1
@@ -1,15 +1,23 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import ProductCard from "../components/ProductCard.astro";
|
||||
import HeartRating from "../components/HeartRating.astro";
|
||||
import { categories } from "../data/categories";
|
||||
import { products } from "../data/products";
|
||||
import { products, alleBewertungen, gesamtDurchschnitt } from "../data/products";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
import type { Locale } from "../i18n/config";
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
|
||||
const neuheiten = products.filter((p) => p.badges.includes("neu")).slice(0, 4);
|
||||
const bestseller = products.filter((p) => p.badges.includes("bestseller")).slice(0, 4);
|
||||
const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).slice(0, 4);
|
||||
// Kundenstimmen-Sektion — erscheint erst automatisch, sobald VanVan im Adminbereich die erste
|
||||
// ECHTE Rezension einträgt (siehe Bewertung-Feld bei den Produkten). Bewusst KEINE erfundenen
|
||||
// Beispiel-Bewertungen im Code, da Fake-Bewertungen in Deutschland/der EU verboten sind.
|
||||
const topBewertungen = alleBewertungen().slice(0, 3);
|
||||
const bewertungSchnittGesamt = gesamtDurchschnitt();
|
||||
---
|
||||
<Layout title="Startseite" lang={lang} path="/">
|
||||
<section class="hero">
|
||||
@@ -84,6 +92,31 @@ const sale = products.filter((p) => p.badges.includes("sale") || p.preisAlt).sli
|
||||
</section>
|
||||
)}
|
||||
|
||||
{topBewertungen.length > 0 && bewertungSchnittGesamt !== null && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<span class="eyebrow">🩵 {t.reviews.heading}</span>
|
||||
<h2>{t.reviews.heading}</h2>
|
||||
<p class="lead">{t.reviews.lead}</p>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnittGesamt} size={24} showValue={true} anzahlLabel={t.reviews.basedOn(alleBewertungen().length)} />
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
{topBewertungen.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
<a class="small" href={`/produkt/${r.produktSlug}/`}>{t.reviews.toProduct} {r.produktName[lang]}</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section class="section">
|
||||
<div class="container grid grid-2 creator-section">
|
||||
<div class="portrait-frame creator-photo">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import { products, getProduct, productsByCategory } from "../../data/products";
|
||||
import HeartRating from "../../components/HeartRating.astro";
|
||||
import { products, getProduct, productsByCategory, durchschnittsbewertung, anzahlBewertungen } from "../../data/products";
|
||||
import { getCategory } from "../../data/categories";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
@@ -27,6 +28,8 @@ const finalPrice = effektiverPreis(product);
|
||||
const discountBadge = rabattLabel(product);
|
||||
const showOldAsPreis = !product.preisAlt && hatAktivenRabatt(product);
|
||||
const mengenrabatt = product.rabatt?.mengenrabatt ?? [];
|
||||
const bewertungSchnitt = durchschnittsbewertung(product);
|
||||
const bewertungAnzahl = anzahlBewertungen(product);
|
||||
|
||||
const felder: [string, string | undefined][] = [
|
||||
[t.product.articleNo, product.artikelnummer],
|
||||
@@ -71,6 +74,11 @@ const felder: [string, string | undefined][] = [
|
||||
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
|
||||
</div>
|
||||
<h1>{name}</h1>
|
||||
{bewertungSchnitt !== null && (
|
||||
<a href="#bewertungen" class="review-summary-link">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={16} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</a>
|
||||
)}
|
||||
<div class="price-row-detail">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
{showOldAsPreis && <span class="price-old">{formatPrice(product.preis, lang)}</span>}
|
||||
@@ -123,6 +131,33 @@ const felder: [string, string | undefined][] = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight" id="bewertungen">
|
||||
<div class="container">
|
||||
<h2>{t.product.reviewsTitle}</h2>
|
||||
{bewertungSchnitt !== null ? (
|
||||
<>
|
||||
<div class="review-summary-row">
|
||||
<HeartRating bewertung={bewertungSchnitt} size={26} showValue={true} anzahlLabel={t.product.reviewCount(bewertungAnzahl)} />
|
||||
</div>
|
||||
<div class="review-list">
|
||||
{product.bewertungen!.map((r) => (
|
||||
<div class="card review-card">
|
||||
<div class="review-card-head">
|
||||
<span class="review-name">{r.name}</span>
|
||||
<HeartRating bewertung={r.bewertung} size={14} />
|
||||
{r.datum && <span class="review-date small">{r.datum}</span>}
|
||||
</div>
|
||||
{r.text && <p class="small">{r.text[lang]}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p class="small">{t.product.noReviews}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{empfehlungen.length > 0 && (
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import { categories, subcategoryChipLabel } from "../../data/categories";
|
||||
import { products } from "../../data/products";
|
||||
import { products, durchschnittsbewertung } from "../../data/products";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
|
||||
@@ -64,6 +64,16 @@ const t = useTranslations(lang);
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-bewertung">{t.shop.minRatingLabel}</label>
|
||||
<select id="f-bewertung">
|
||||
<option value="0">{t.shop.minRatingAny}</option>
|
||||
<option value="4">🩵🩵🩵🩵 +</option>
|
||||
<option value="3">🩵🩵🩵 +</option>
|
||||
<option value="2">🩵🩵 +</option>
|
||||
<option value="1">🩵 +</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="small">{t.shop.filterNote}</p>
|
||||
</aside>
|
||||
|
||||
@@ -73,6 +83,7 @@ const t = useTranslations(lang);
|
||||
<select id="sort" aria-label={t.shop.sortLabel}>
|
||||
<option value="neu">{t.shop.sortNew}</option>
|
||||
<option value="bestseller">{t.shop.sortBestseller}</option>
|
||||
<option value="bewertung">{t.shop.sortRating}</option>
|
||||
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
|
||||
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
|
||||
</select>
|
||||
@@ -87,6 +98,7 @@ const t = useTranslations(lang);
|
||||
data-bestand={p.bestand}
|
||||
data-neu={p.badges.includes("neu") ? 1 : 0}
|
||||
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
|
||||
data-bewertung={durchschnittsbewertung(p) ?? 0}
|
||||
>
|
||||
<ProductCard product={p} lang={lang} />
|
||||
</div>
|
||||
@@ -106,6 +118,7 @@ const t = useTranslations(lang);
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const bewertungSel = document.getElementById("f-bewertung");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
const emptyMsg = document.getElementById("empty-msg");
|
||||
@@ -119,6 +132,7 @@ const t = useTranslations(lang);
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
const minBewertung = Number(bewertungSel.value);
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
let visible = 0;
|
||||
@@ -130,7 +144,8 @@ const t = useTranslations(lang);
|
||||
: slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar;
|
||||
const matchBewertung = Number(slot.dataset.bewertung) >= minBewertung;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar && matchBewertung;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
@@ -143,13 +158,14 @@ const t = useTranslations(lang);
|
||||
case "preis-auf": return Number(a.dataset.preis) - Number(b.dataset.preis);
|
||||
case "preis-ab": return Number(b.dataset.preis) - Number(a.dataset.preis);
|
||||
case "bestseller": return Number(b.dataset.bestseller) - Number(a.dataset.bestseller);
|
||||
case "bewertung": return Number(b.dataset.bewertung) - Number(a.dataset.bewertung);
|
||||
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
|
||||
}
|
||||
});
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
[preisRange, verfuegbarChk, bewertungSel, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
// Eigenes Kategorie-Dropdown (statt nativem <select>): Klick auf den Button öffnet/schließt
|
||||
// die Liste, Klick auf eine Zeile setzt den Filter, aktualisiert die Anzeige-Beschriftung und
|
||||
|
||||
Reference in New Issue
Block a user