Initial commit: Van's DIY & Bastelbedarf Astro shop

This commit is contained in:
qcigano
2026-08-02 13:22:55 +02:00
commit 2e340f7a8a
92 changed files with 11022 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { categories } from "../../../data/categories";
import { products } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Boutique" description="La boutique complète Van's DIY & Bastelbedarf crochet, peluches, bijoux et fournitures créatives." lang={lang} path="/shop/">
<div class="container">
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
<div class="page-banner-content">
<span class="eyebrow">{t.shop.breadcrumbShop}</span>
<h1>{t.shop.title}</h1>
<p class="lead">{t.shop.lead}</p>
</div>
</section>
</div>
<section class="section-tight">
<div class="container shop-layout">
<aside class="filters card" aria-label={t.shop.filters}>
<h3>{t.shop.filters}</h3>
<div class="filter-group">
<label for="f-kategorie">{t.shop.categoryLabel}</label>
<select id="f-kategorie">
<option value="">{t.shop.allCategories}</option>
{categories.map((c) => <option value={c.slug}>{c.name[lang]}</option>)}
</select>
</div>
<div class="filter-group">
<label for="f-preis">{t.shop.priceUpTo}</label>
<input id="f-preis" type="range" min="0" max="110" value="110" />
<span class="small" id="f-preis-value">{t.shop.priceUpToValue(110)}</span>
</div>
<div class="filter-group">
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
<input id="f-verfuegbar" type="checkbox" />
</div>
<p class="small">{t.shop.filterNote}</p>
</aside>
<div>
<div class="shop-toolbar">
<span class="small" id="result-count">{t.shop.resultCount(products.length)}</span>
<select id="sort" aria-label={t.shop.sortLabel}>
<option value="neu">{t.shop.sortNew}</option>
<option value="bestseller">{t.shop.sortBestseller}</option>
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
</select>
</div>
<div class="grid grid-3" id="product-grid">
{products.map((p) => (
<div
class="product-slot"
data-kategorie={p.kategorie}
data-preis={p.preis}
data-bestand={p.bestand}
data-neu={p.badges.includes("neu") ? 1 : 0}
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
>
<ProductCard product={p} lang={lang} />
</div>
))}
</div>
<p class="small" id="empty-msg" style="display:none;">{t.shop.noResults}</p>
<p class="legal-hint small">{t.common.vatNote} {t.common.allPricesPlusShipping}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ productSingular: t.shop.productSingular, productPlural: t.shop.productPlural, unitFirst: lang === "en" }}>
const grid = document.getElementById("product-grid");
const slots = Array.from(grid.querySelectorAll(".product-slot"));
const kategorieSel = document.getElementById("f-kategorie");
const preisRange = document.getElementById("f-preis");
const preisValue = document.getElementById("f-preis-value");
const verfuegbarChk = document.getElementById("f-verfuegbar");
const sortSel = document.getElementById("sort");
const resultCount = document.getElementById("result-count");
const emptyMsg = document.getElementById("empty-msg");
const currencySymbol = "€";
function formatPrice(v) {
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
}
function apply() {
const kat = kategorieSel.value;
const maxPreis = Number(preisRange.value);
const nurVerfuegbar = verfuegbarChk.checked;
preisValue.textContent = formatPrice(maxPreis);
let visible = 0;
slots.forEach((slot) => {
const matchKat = !kat || slot.dataset.kategorie === kat;
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
const show = matchKat && matchPreis && matchVerfuegbar;
slot.style.display = show ? "" : "none";
if (show) visible++;
});
resultCount.textContent = `${visible} ${visible === 1 ? productSingular : productPlural}`;
emptyMsg.style.display = visible === 0 ? "block" : "none";
const sorted = [...slots].sort((a, b) => {
switch (sortSel.value) {
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);
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
}
});
sorted.forEach((el) => grid.appendChild(el));
}
[kategorieSel, preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
const params = new URLSearchParams(location.search);
const preset = params.get("kategorie");
if (preset) kategorieSel.value = preset;
apply();
</script>