Initial commit: Van's DIY & Bastelbedarf Astro shop
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import { categories, getCategory } from "../../data/categories";
|
||||
import { productsByCategory } from "../../data/products";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
|
||||
export function getStaticPaths() {
|
||||
return categories.map((c) => ({ params: { kategorie: c.slug } }));
|
||||
}
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
const { kategorie } = Astro.params;
|
||||
const category = getCategory(kategorie!)!;
|
||||
const items = productsByCategory(category.slug);
|
||||
---
|
||||
<Layout title={category.name[lang]} description={category.description[lang]} lang={lang} path={`/shop/${category.slug}/`}>
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small breadcrumb"><a href="/shop/">{t.shop.breadcrumbShop}</a> / {category.name[lang]}</p>
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon}</span>
|
||||
<h1>{category.name[lang]}</h1>
|
||||
<p class="lead">{category.description[lang]}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{category.subcategories && category.subcategories.length > 0 && (
|
||||
<>
|
||||
<h2 class="text-center small" style="text-transform:uppercase; letter-spacing:0.08em; color:var(--c-text-muted); margin-bottom:0.8rem;">{t.shop.subcategoriesLabel}</h2>
|
||||
<div class="subcategory-chips">
|
||||
{category.subcategories.map((s) => (
|
||||
<a class="subcategory-chip" href={`/shop/${category.slug}/${s.slug}/`}>{s.name[lang]}</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{items.length > 0 ? (
|
||||
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
) : (
|
||||
<p class="small">{t.shop.categoryEmpty}</p>
|
||||
)}
|
||||
<p class="legal-hint small">{t.common.vatNote} {t.common.allPricesPlusShipping}</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import { categories, getCategory, getSubcategory } from "../../../data/categories";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
|
||||
export function getStaticPaths() {
|
||||
return categories.flatMap((c) =>
|
||||
(c.subcategories ?? []).map((s) => ({ params: { kategorie: c.slug, unterkategorie: s.slug } }))
|
||||
);
|
||||
}
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
const { kategorie, unterkategorie } = Astro.params;
|
||||
const category = getCategory(kategorie!)!;
|
||||
const sub = getSubcategory(kategorie!, unterkategorie!)!;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} bei Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small breadcrumb">
|
||||
<a href="/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/shop/${category.slug}/`}>{category.name[lang]}</a> / {sub.name[lang]}
|
||||
</p>
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
<div class="btn-row">
|
||||
<a class="btn btn-outline" href={`/shop/${category.slug}/`}>← {category.name[lang]}</a>
|
||||
<a class="btn btn-primary" href="/shop/">{t.common.toShop}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -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 = "de";
|
||||
const t = useTranslations(lang);
|
||||
---
|
||||
<Layout title="Shop" description="Der komplette Shop von Van's DIY & Bastelbedarf – Häkelwerke, Plushies, Schmuck und Bastelzubehör." 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>
|
||||
Reference in New Issue
Block a user