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
+74
View File
@@ -0,0 +1,74 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="Allgemeine Geschäftsbedingungen (AGB)" path="/agb/" legalOnly={true}>
<section class="section-tight">
<div class="container">
<h1>Allgemeine Geschäftsbedingungen</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung durch eine fachkundige Stelle vor Veröffentlichung.
</div>
<h2>§ 1 Geltungsbereich</h2>
<p>
Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen, die Verbraucher über
den Onlineshop von Van's DIY & Bastelbedarf (Einzelunternehmen, [Anschrift]) aufgeben.
</p>
<h2>§ 2 Vertragspartner, Vertragsschluss</h2>
<p>
Der Kaufvertrag kommt zustande mit Van's DIY & Bastelbedarf. Die Darstellung der Produkte
im Shop stellt kein rechtlich bindendes Angebot dar, sondern eine unverbindliche
Aufforderung, ein Angebot abzugeben. Mit Anklicken des Buttons „Jetzt zahlungspflichtig
bestellen" gibst du ein verbindliches Angebot zum Kauf ab. Wir bestätigen den Eingang
deiner Bestellung per E-Mail; diese Bestätigung stellt noch keine Vertragsannahme dar. Der
Vertrag kommt erst mit unserer gesonderten Auftragsbestätigung oder Versand der Ware
zustande.
</p>
<h2>§ 3 Preise und Versandkosten</h2>
<p>
Alle angegebenen Preise verstehen sich als Endpreise. Gemäß §19 UStG wird als
Kleinunternehmer keine Umsatzsteuer berechnet und ausgewiesen. Es kommen ggf.
Versandkosten hinzu, die im Bestellprozess vor Abschluss der Bestellung ausgewiesen
werden. Ab einem Warenwert von 75 € liefern wir versandkostenfrei.
</p>
<h2>§ 4 Zahlung</h2>
<p>Die Zahlung erfolgt wahlweise per PayPal, Klarna oder Banküberweisung (Vorkasse).</p>
<h2>§ 5 Lieferung</h2>
<p>
Die Lieferung erfolgt ausschließlich mit DHL innerhalb Deutschlands, Österreichs, der
Schweiz und Luxemburgs. Die Lieferzeit beträgt in der Regel 27 Werktage nach
Zahlungseingang.
</p>
<h2>§ 6 Eigentumsvorbehalt</h2>
<p>Bis zur vollständigen Bezahlung bleibt die gelieferte Ware unser Eigentum.</p>
<h2>§ 7 Widerrufsrecht</h2>
<p>
Es gilt das gesetzliche Widerrufsrecht für Verbraucher gemäß unserer gesonderten
Widerrufsbelehrung. Bei Waren, die nach Kundenspezifikation angefertigt oder eindeutig auf
die persönlichen Bedürfnisse zugeschnitten sind, kann das Widerrufsrecht gemäß § 312g Abs.
2 Nr. 1 BGB ausgeschlossen sein.
</p>
<h2>§ 8 Gewährleistung</h2>
<p>Es gilt das gesetzliche Mängelhaftungsrecht.</p>
<h2>§ 9 Streitbeilegung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a>.
Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
<h2>§ 10 Schlussbestimmungen</h2>
<p>Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des UN-Kaufrechts.</p>
</div>
</section>
</Layout>
+23
View File
@@ -0,0 +1,23 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title={t.checkout.thankYouTitle} lang={lang} path="/checkout/danke/">
<section class="section">
<div class="container text-center danke">
<span class="eyebrow">🩵 {t.checkout.thankYouEyebrow}!</span>
<h1>{t.checkout.thankYouTitle}</h1>
<p class="lead">{t.checkout.thankYouLead}</p>
<a class="btn btn-primary" href="/ch/shop/">{t.checkout.continueShopping}</a>
</div>
</section>
</Layout>
<script>
localStorage.removeItem("vandiy_cart_v1");
window.dispatchEvent(new CustomEvent("cart:changed"));
</script>
+99
View File
@@ -0,0 +1,99 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Checkout" description="Bschtellig abschliesse bi Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.checkout.eyebrow}</span>
<h1>{t.checkout.title}</h1>
<p class="todo-note">{t.checkout.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container checkout-layout">
<form class="frm card" id="checkout-form">
<h3>{t.checkout.step1}</h3>
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
<div class="grid grid-2">
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
</div>
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="Bahnhofstrasse 1" required /></div>
<div class="grid grid-2">
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="4" placeholder="8000" required /></div>
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="Zürich" required /></div>
</div>
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option selected>Schwiz</option>
<option>Dütschland</option>
<option>Öschterrych</option>
<option>Luxemburg</option>
</select>
</div>
<hr class="divider" />
<h3>{t.checkout.step2}</h3>
<div class="payment-options">
<label class="pay-option"><input type="radio" name="pay" value="paypal" checked /> PayPal</label>
<label class="pay-option"><input type="radio" name="pay" value="klarna" /> Klarna</label>
<label class="pay-option"><input type="radio" name="pay" value="ueberweisung" /> {t.checkout.bankTransfer}</label>
</div>
<hr class="divider" />
<h3>{t.checkout.step3}</h3>
<div id="checkout-summary" class="checkout-summary"></div>
<p class="small">{t.common.vatNote}</p>
<div class="checkbox-row">
<input id="agb" type="checkbox" required />
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
</div>
<div class="checkbox-row">
<input id="widerruf" type="checkbox" required />
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
</form>
<aside class="card checkout-aside">
<h3>{t.checkout.shippingHintTitle}</h3>
<p class="small">{t.checkout.shippingHint}</p>
<a class="small" href="/ch/versand-zahlung/">{t.checkout.shippingLink}</a>
</aside>
</div>
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/ch/checkout/danke/", checkoutLang: lang }}>
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const summary = document.getElementById("checkout-summary");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
if (cart.length === 0) {
alert(emptyCartAlert);
return;
}
location.href = thankYouPath;
});
</script>
+26
View File
@@ -0,0 +1,26 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="FAQ" description="Häufigi Frage zu Van's DIY & Bastelbedarf." lang={lang} path="/faq/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">FAQ</span>
<h1>{t.faq.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container faq-list">
{t.faq.items.map((f) => (
<details class="card faq-item">
<summary>{f.q}</summary>
<p class="small">{f.a}</p>
</details>
))}
</div>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
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";
const lang: Locale = "ch";
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);
---
<Layout title="Startsyte" lang={lang} path="/">
<section class="hero">
<img src="/logo.png" alt="" aria-hidden="true" class="hero-watermark" />
<div class="container hero-content">
<div class="hero-gallery">
<div class="hero-photo hero-photo-1"><img src="/img/hero-plushie-bunny.webp" alt="Ghäkleti Häsli-Plushie mit blauer Latzhose, im Gras" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-2"><img src="/img/hero-plushie-turtle.webp" alt="Ghäkleti Schildchröte-Plushie i Hellblau und Gäl" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-3"><img src="/img/hero-plushie-octopus.webp" alt="Ghäklets Oktopus-Plushie i Rosa" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-4"><img src="/img/hero-plushie-penguin.webp" alt="Ghäklets Pinguin-Plushie i Hellblau und Wiss" loading="eager" width="900" height="1200" /></div>
</div>
<div class="hero-inner">
<span class="eyebrow">🩵 Van's DIY & Bastelbedarf</span>
<h1>Vo Hang gmacht mit Härz <br />Sächeli zum Verschänke<br />und Sälber Bhalte.</h1>
<p class="lead">Häkelwerk, Plushies, Schmuck und Bastelzüg jedes Sächeli vo Hang gmacht, mit Härz und Geduld.</p>
<div class="btn-row">
<a class="btn btn-primary" href="/ch/shop/">Jetzt use luege</a>
<a class="btn btn-outline" href="/ch/ueber-die-kreative/">Über die Kreative</a>
</div>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">Kategorie entdecke</h2>
<div class="grid grid-3" style="margin-top:1.5rem;">
{categories.map((c) => (
<a class="card category-card" href={`/ch/shop/${c.slug}/`}>
<span class="cat-icon" aria-hidden="true">{c.icon}</span>
<h3>{c.name[lang]}</h3>
<p class="small">{c.description[lang]}</p>
</a>
))}
</div>
</div>
</section>
{neuheiten.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Neui Sache</h2>
<a class="small" href="/ch/shop/">Alles aluege →</a>
</div>
<div class="grid grid-4">{neuheiten.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{bestseller.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Bestseller</h2>
<a class="small" href="/ch/shop/">Alles aluege →</a>
</div>
<div class="grid grid-4">{bestseller.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{sale.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Aktion %</h2>
<a class="small" href="/ch/sale/">Alle Angebote →</a>
</div>
<div class="grid grid-4">{sale.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section">
<div class="container grid grid-2 creator-section">
<div class="portrait-frame creator-photo">
<img src="/img/vanvan-portrait.png" alt="Porträtfoto von VanVan" width="640" height="640" loading="lazy" />
<span class="portrait-badge">🩵 Handgemacht</span>
</div>
<div>
<span class="eyebrow">Die Kreative hinter em Shop</span>
<h2>Hoi zäme, ich bin VanVan 🩵</h2>
<blockquote class="pull-quote">«Jedi Masche verzellt ihri eigeti Gschicht wil echti Handarbeit entsteit nid nur mit Wolle, sondern vor allem mit ganz viel Härz.»</blockquote>
<a class="btn btn-outline" href="/ch/ueber-die-kreative/">Mehr über mich →</a>
</div>
</div>
</section>
</Layout>
+66
View File
@@ -0,0 +1,66 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Kontakt" description="Kontaktier Van's DIY & Bastelbedarf." lang={lang} path="/kontakt/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.nav.contact}</span>
<h1>{t.contact.title}</h1>
<p class="lead">{t.contact.lead}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 contact-layout">
<form class="frm card" id="contact-form">
<div>
<label for="name">{t.contact.name}</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">{t.contact.email}</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="betreff">{t.contact.subject}</label>
<input id="betreff" name="betreff" type="text" />
</div>
<div>
<label for="nachricht">{t.contact.message}</label>
<textarea id="nachricht" name="nachricht" rows="5" required></textarea>
</div>
<div class="checkbox-row">
<input id="dsgvo" type="checkbox" required />
<label for="dsgvo">{t.contact.gdprPrefix} <a href="/datenschutz/">{t.contact.gdprLink}</a> {t.contact.gdprSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.contact.send}</button>
<p class="small" id="form-status" role="status"></p>
<p class="todo-note">{t.contact.todoNote}</p>
</form>
<div>
<h3>{t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:[email protected]">[email protected]</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3>{t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ sentDemo: t.contact.sentDemo }}>
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", (e) => {
e.preventDefault();
status.textContent = sentDemo;
status.style.color = "var(--c-accent)";
form.reset();
});
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Mis Konto" description="Gastbstellig oder Kundekonto bi Van's DIY & Bastelbedarf." lang={lang} path="/konto/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.account.eyebrow}</span>
<h1>{t.account.title}</h1>
<p class="todo-note">{t.account.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 account-grid">
<form class="frm card">
<h3>{t.account.loginTitle}</h3>
<div><label for="login-email">{t.account.email}</label><input id="login-email" type="email" /></div>
<div><label for="login-pw">{t.account.password}</label><input id="login-pw" type="password" /></div>
<button class="btn btn-primary btn-block" type="button" disabled>{t.account.loginButton}</button>
<a class="small" href="#">{t.account.forgotPassword}</a>
</form>
<div class="card">
<h3>{t.account.newTitle}</h3>
<p class="small">{t.account.newText}</p>
<a class="btn btn-outline btn-block" href="/ch/shop/">{t.account.guestButton}</a>
</div>
</div>
</section>
</Layout>
+121
View File
@@ -0,0 +1,121 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { products, getProduct, productsByCategory } from "../../../data/products";
import { getCategory } from "../../../data/categories";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
export function getStaticPaths() {
return products.map((p) => ({ params: { slug: p.slug } }));
}
const lang: Locale = "ch";
const t = useTranslations(lang);
const { slug } = Astro.params;
const product = getProduct(slug!)!;
const category = getCategory(product.kategorie);
const empfehlungen = productsByCategory(product.kategorie).filter((p) => p.slug !== product.slug).slice(0, 3);
const name = product.name[lang];
const desc = product.beschreibung[lang];
const badgeLabel: Record<string, string> = { neu: t.badge.neu, bestseller: t.badge.bestseller, sale: t.badge.sale, handgemacht: t.badge.handgemacht };
const felder: [string, string | undefined][] = [
[t.product.articleNo, product.artikelnummer],
[t.product.material, product.material],
[t.product.size, product.groesse],
[t.product.colors, product.farben?.join(", ")],
[t.product.included, product.lieferumfang],
[t.product.care, product.pflegehinweise],
[t.product.delivery, product.lieferzeit],
];
---
<Layout title={name} description={desc} lang={lang} path={`/produkt/${product.slug}/`}>
<section class="section-tight">
<div class="container">
<p class="small breadcrumb">
<a href="/ch/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/ch/shop/${product.kategorie}/`}>{category?.name[lang]}</a> / {name}
</p>
<div class="grid grid-2 product-detail">
<div>
<div class="img-placeholder product-hero" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
{t.common.photoSoon}<br /><span class="small">{t.product.photoNote}</span>
</div>
<div class="thumb-row">
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
</div>
</div>
<div>
<div class="badge-row">
{product.badges.map((b) => <span class={`badge ${b === "sale" ? "badge-sale" : ""}`}>{badgeLabel[b]}</span>)}
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
</div>
<h1>{name}</h1>
<div class="price-row-detail">
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
<span class="price-detail">{formatPrice(product.preis, lang)}</span>
</div>
<p class="small">{t.common.vatNote} · {t.common.plusShipping}</p>
<p class="lead">{desc}</p>
<div class="qty-row">
<label for="qty">{t.common.quantity}</label>
<input type="number" id="qty" min="1" value="1" />
<button
class="btn btn-primary"
id="add-to-cart"
disabled={product.bestand === 0}
data-slug={product.slug}
data-name={name}
data-preis={product.preis}
>
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
</button>
</div>
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
{felder.some(([, v]) => v) && (
<table class="specs">
<tbody>
{felder.filter(([, v]) => v).map(([k, v]) => (
<tr><th>{k}</th><td>{v}</td></tr>
))}
</tbody>
</table>
)}
<div class="todo-note">{t.product.todoFields}</div>
</div>
</div>
</div>
</section>
{empfehlungen.length > 0 && (
<section class="section-tight">
<div class="container">
<h2>{t.product.recommendations}</h2>
<div class="grid grid-3">{empfehlungen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
</Layout>
<script>
import { addToCart } from "../../../scripts/cart";
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
const qtyInput = document.getElementById("qty") as HTMLInputElement;
const confirm = document.getElementById("add-confirm")!;
btn?.addEventListener("click", () => {
const { slug, name, preis } = btn.dataset;
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
confirm.style.display = "block";
setTimeout(() => (confirm.style.display = "none"), 2500);
});
</script>
+31
View File
@@ -0,0 +1,31 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { saleProducts } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
const items = saleProducts();
---
<Layout title="Aktion" description="Aktuelli Aktion bi Van's DIY & Bastelbedarf." lang={lang} path="/sale/">
<div class="container">
<section class="page-banner" style="background-image:url('/img/banner-sale.webp'); --banner-ratio: 1600 / 639;">
<div class="page-banner-content">
<span class="eyebrow">{t.sale.eyebrow}</span>
<h1>{t.sale.title}</h1>
<p class="lead">{t.sale.lead}</p>
</div>
</section>
</div>
<section class="section-tight">
<div class="container">
{items.length > 0 ? (
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
) : (
<p class="small">{t.sale.empty}</p>
)}
</div>
</section>
</Layout>
+49
View File
@@ -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 = "ch";
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="/ch/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={`/ch/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 = "ch";
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="/ch/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/ch/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={`/ch/shop/${category.slug}/`}>← {category.name[lang]}</a>
<a class="btn btn-primary" href="/ch/shop/">{t.common.toShop}</a>
</div>
</div>
</section>
</Layout>
+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 = "ch";
const t = useTranslations(lang);
---
<Layout title="Shop" description="Dr ganz Shop vo Van's DIY & Bastelbedarf Häkelwerk, Plushies, Schmuck und Bastelzüg." 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>
@@ -0,0 +1,83 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Über d'Kreativi" description="Lern VanVan kenne d'Person hinger Van's DIY & Bastelbedarf." lang={lang} path="/ueber-die-kreative/">
<section class="section-tight">
<div class="container grid grid-2 intro">
<div class="portrait-frame portrait">
<img src="/img/vanvan-portrait-holding.webp" alt="VanVan i ihrer Werkstatt mit ihre sälber ghäklete Plushies" width="900" height="1353" loading="lazy" />
<span class="portrait-badge">{t.common.handmadeBadge}</span>
</div>
<div>
<span class="eyebrow">{t.about.eyebrow}</span>
<h1>{t.about.title}</h1>
<p class="lead">{t.about.lead}</p>
</div>
</div>
</section>
<section class="section-tight">
<div class="container story">
{t.about.story.map((s) => (
<div class="story-section">
<h2>{s.title}</h2>
{s.paragraphs.map((p) => <p>{p}</p>)}
</div>
))}
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">{t.about.valuesTitle}</h2>
<div class="grid grid-3">
{t.about.values.map((v) => (
<div class="card text-center">
<h3>{v.icon} {v.title}</h3>
<p class="small">{v.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<blockquote class="pull-quote pull-quote-center">„{t.about.closing}”</blockquote>
</div>
</section>
<section class="section-tight">
<div class="container text-center">
<a class="btn btn-primary" href="/ch/shop/">{t.common.toShop}</a>
</div>
</section>
</Layout>
<script>
// Dezente Scroll-Animation für die Story-Abschnitte (Wunsch aus Dokument1.pdf: "sanfte
// Scroll-Animationen sind willkommen"). Fällt sauber zurück, falls IntersectionObserver
// fehlt oder der Nutzer reduzierte Bewegung bevorzugt (siehe CSS prefers-reduced-motion).
const sections = document.querySelectorAll(".story-section");
if ("IntersectionObserver" in window && sections.length) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.15 }
);
sections.forEach((el) => io.observe(el));
} else {
sections.forEach((el) => el.classList.add("is-visible"));
}
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Versand & Zahlig" description="Versandbedingige und Zahligsarte bi Van's DIY & Bastelbedarf." lang={lang} path="/versand-zahlung/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.shipping.eyebrow}</span>
<h1>{t.shipping.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2">
<div class="card">
<h3>{t.shipping.shippingTitle}</h3>
<ul>
{t.shipping.shippingItems.map((item) => <li>{item}</li>)}
</ul>
</div>
<div class="card">
<h3>{t.shipping.paymentTitle}</h3>
<ul>
{t.shipping.paymentItems.map((item) => <li>{item}</li>)}
</ul>
<p class="small">{t.common.vatNote}</p>
</div>
</div>
<p class="todo-note">{t.shipping.todoNote}</p>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "ch";
const t = useTranslations(lang);
---
<Layout title="Warenchorb" description="Din Warenchorb bi Van's DIY & Bastelbedarf." lang={lang} path="/warenkorb/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.cart.eyebrow}</span>
<h1>{t.cart.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container">
<div id="cart-empty" class="card" style="display:none;">
<p>{t.cart.empty}</p>
<a class="btn btn-primary" href="/ch/shop/">{t.common.browseShop}</a>
</div>
<div id="cart-content" class="cart-layout" style="display:none;">
<div class="card cart-items" id="cart-items"></div>
<aside class="card cart-summary">
<h3>{t.cart.subtotal}</h3>
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
<div class="summary-row"><span>{t.cart.shipping}</span><span id="sum-shipping">{t.cart.shippingCalculated}</span></div>
<p class="small" id="shipping-hint"></p>
<hr class="divider" />
<div class="summary-row total"><span>{t.cart.total}</span><span id="sum-total">0,00 €</span></div>
<p class="small">{t.common.vatNote}</p>
<a class="btn btn-primary btn-block" href="/ch/checkout/">{t.cart.toCheckout}</a>
</aside>
</div>
</div>
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
const content = document.getElementById("cart-content");
const itemsEl = document.getElementById("cart-items");
if (cart.length === 0) {
empty.style.display = "block";
content.style.display = "none";
return;
}
empty.style.display = "none";
content.style.display = "grid";
itemsEl.innerHTML = cart.map((i) => `
<div class="cart-item">
<div class="img-placeholder thumb"></div>
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(i.preis, cartLang)} ${perItemLabel}</div>
</div>
<div class="qty-controls">
<button data-action="dec" data-slug="${i.slug}"></button>
<span>${i.menge}</span>
<button data-action="inc" data-slug="${i.slug}">+</button>
</div>
<div><strong>${formatPrice(i.preis * i.menge, cartLang)}</strong></div>
<a href="#" class="remove" data-action="remove" data-slug="${i.slug}">${removeLabel}</a>
</div>
`).join("");
const subtotal = cartTotal();
const remaining = Math.max(0, freeShipFrom - subtotal);
document.getElementById("sum-subtotal").textContent = formatPrice(subtotal, cartLang);
document.getElementById("sum-total").textContent = formatPrice(subtotal, cartLang);
document.getElementById("shipping-hint").textContent =
remaining > 0 ? remainingTemplate.replace("__V__", formatPrice(remaining, cartLang)) : freeShippingText;
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const target = e.currentTarget;
const slug = target.dataset.slug;
const action = target.dataset.action;
const item = getCart().find((i) => i.slug === slug);
if (!item) return;
if (action === "inc") updateQuantity(slug, item.menge + 1);
if (action === "dec") updateQuantity(slug, item.menge - 1);
if (action === "remove") removeFromCart(slug);
render();
});
});
}
render();
window.addEventListener("cart:changed", render);
</script>
+23
View File
@@ -0,0 +1,23 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title={t.checkout.thankYouTitle} lang={lang} path="/checkout/danke/">
<section class="section">
<div class="container text-center danke">
<span class="eyebrow">🩵 {t.checkout.thankYouEyebrow}!</span>
<h1>{t.checkout.thankYouTitle}</h1>
<p class="lead">{t.checkout.thankYouLead}</p>
<a class="btn btn-primary" href="/shop/">{t.checkout.continueShopping}</a>
</div>
</section>
</Layout>
<script>
localStorage.removeItem("vandiy_cart_v1");
window.dispatchEvent(new CustomEvent("cart:changed"));
</script>
+99
View File
@@ -0,0 +1,99 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Checkout" description="Bestellung abschließen bei Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.checkout.eyebrow}</span>
<h1>{t.checkout.title}</h1>
<p class="todo-note">{t.checkout.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container checkout-layout">
<form class="frm card" id="checkout-form">
<h3>{t.checkout.step1}</h3>
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
<div class="grid grid-2">
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
</div>
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="Musterstraße 1" required /></div>
<div class="grid grid-2">
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="5" placeholder="10115" required /></div>
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="Berlin" required /></div>
</div>
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Deutschland</option>
<option>Österreich</option>
<option>Schweiz</option>
<option>Luxemburg</option>
</select>
</div>
<hr class="divider" />
<h3>{t.checkout.step2}</h3>
<div class="payment-options">
<label class="pay-option"><input type="radio" name="pay" value="paypal" checked /> PayPal</label>
<label class="pay-option"><input type="radio" name="pay" value="klarna" /> Klarna</label>
<label class="pay-option"><input type="radio" name="pay" value="ueberweisung" /> {t.checkout.bankTransfer}</label>
</div>
<hr class="divider" />
<h3>{t.checkout.step3}</h3>
<div id="checkout-summary" class="checkout-summary"></div>
<p class="small">{t.common.vatNote}</p>
<div class="checkbox-row">
<input id="agb" type="checkbox" required />
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
</div>
<div class="checkbox-row">
<input id="widerruf" type="checkbox" required />
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
</form>
<aside class="card checkout-aside">
<h3>{t.checkout.shippingHintTitle}</h3>
<p class="small">{t.checkout.shippingHint}</p>
<a class="small" href="/versand-zahlung/">{t.checkout.shippingLink}</a>
</aside>
</div>
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/checkout/danke/", checkoutLang: lang }}>
import { getCart, cartTotal } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
const summary = document.getElementById("checkout-summary");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
if (cart.length === 0) {
alert(emptyCartAlert);
return;
}
location.href = thankYouPath;
});
</script>
+70
View File
@@ -0,0 +1,70 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="Datenschutzerklärung" path="/datenschutz/" legalOnly={true}>
<section class="section-tight">
<div class="container">
<h1>Datenschutzerklärung</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung. Vor Veröffentlichung an die tatsächlich eingesetzten Dienste (z. B. konkreter
Hosting-Anbieter, Zahlungsdienstleister, Versandtool) anpassen und rechtlich prüfen lassen.
</div>
<h2>1. Verantwortlicher</h2>
<p>[Vorname Nachname], Van's DIY & Bastelbedarf, [Anschrift], [E-Mail-Adresse]</p>
<h2>2. Allgemeines zur Datenverarbeitung</h2>
<p>
Wir verarbeiten personenbezogene Daten unserer Nutzer grundsätzlich nur, soweit dies zur
Bereitstellung einer funktionsfähigen Website sowie unserer Inhalte und Leistungen
erforderlich ist, auf Grundlage der Art. 6 Abs. 1 lit. a, b und f DSGVO.
</p>
<h2>3. Hosting</h2>
<p>
Diese Website wird bei einem externen Dienstleister gehostet (Hoster). Die personenbezogenen
Daten, die auf dieser Website erfasst werden, werden auf den Servern des Hosters
gespeichert. [Konkreten Hosting-Anbieter, z. B. Cloudflare Pages, hier ergänzen inkl.
Standort der Server und ggf. Auftragsverarbeitungsvertrag.]
</p>
<h2>4. Bestellabwicklung</h2>
<p>
Zur Abwicklung deiner Bestellung erheben und verarbeiten wir personenbezogene Daten, wenn
dies zur Erfüllung des Vertrags erforderlich ist (Art. 6 Abs. 1 lit. b DSGVO). Dazu gehören
Name, Anschrift, E-Mail-Adresse und Zahlungsdaten. Die Daten werden ausschließlich zur
Vertragsabwicklung genutzt.
</p>
<h2>5. Zahlungsdienstleister</h2>
<p>
Je nach gewählter Zahlungsart geben wir Daten an die eingesetzten Zahlungsdienstleister
weiter (PayPal, Klarna). Es gelten deren jeweils eigene Datenschutzhinweise. Bei
Banküberweisung werden Daten an unser kontoführendes Kreditinstitut übermittelt.
</p>
<h2>6. Cookies</h2>
<p>
Diese Website verwendet, soweit technisch erforderlich, Cookies zur Bereitstellung des
Warenkorbs. Weitere, nicht zwingend erforderliche Cookies (z. B. Statistik/Marketing)
werden nur nach vorheriger Einwilligung gesetzt. [Cookie-Banner/Consent-Tool ergänzen,
sobald solche Cookies tatsächlich eingesetzt werden.]
</p>
<h2>7. Deine Rechte</h2>
<p>
Du hast jederzeit das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der
Verarbeitung, Datenübertragbarkeit und Widerspruch gegen die Verarbeitung deiner
personenbezogenen Daten sowie das Recht auf Beschwerde bei einer Aufsichtsbehörde.
</p>
<h2>8. Speicherdauer</h2>
<p>
Wir speichern personenbezogene Daten nur so lange, wie es für den jeweiligen Zweck
erforderlich ist oder gesetzliche Aufbewahrungsfristen (insbesondere handels- und
steuerrechtlich) dies vorschreiben.
</p>
</div>
</section>
</Layout>
+23
View File
@@ -0,0 +1,23 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title={t.checkout.thankYouTitle} lang={lang} path="/checkout/danke/">
<section class="section">
<div class="container text-center danke">
<span class="eyebrow">🩵 {t.checkout.thankYouEyebrow}!</span>
<h1>{t.checkout.thankYouTitle}</h1>
<p class="lead">{t.checkout.thankYouLead}</p>
<a class="btn btn-primary" href="/en/shop/">{t.checkout.continueShopping}</a>
</div>
</section>
</Layout>
<script>
localStorage.removeItem("vandiy_cart_v1");
window.dispatchEvent(new CustomEvent("cart:changed"));
</script>
+99
View File
@@ -0,0 +1,99 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="Checkout" description="Complete your order at Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.checkout.eyebrow}</span>
<h1>{t.checkout.title}</h1>
<p class="todo-note">{t.checkout.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container checkout-layout">
<form class="frm card" id="checkout-form">
<h3>{t.checkout.step1}</h3>
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
<div class="grid grid-2">
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
</div>
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="1 Example Street" required /></div>
<div class="grid grid-2">
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="8" placeholder="SW1A 1AA" required /></div>
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="London" required /></div>
</div>
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Germany</option>
<option>Austria</option>
<option>Switzerland</option>
<option>Luxembourg</option>
</select>
</div>
<hr class="divider" />
<h3>{t.checkout.step2}</h3>
<div class="payment-options">
<label class="pay-option"><input type="radio" name="pay" value="paypal" checked /> PayPal</label>
<label class="pay-option"><input type="radio" name="pay" value="klarna" /> Klarna</label>
<label class="pay-option"><input type="radio" name="pay" value="ueberweisung" /> {t.checkout.bankTransfer}</label>
</div>
<hr class="divider" />
<h3>{t.checkout.step3}</h3>
<div id="checkout-summary" class="checkout-summary"></div>
<p class="small">{t.common.vatNote}</p>
<div class="checkbox-row">
<input id="agb" type="checkbox" required />
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
</div>
<div class="checkbox-row">
<input id="widerruf" type="checkbox" required />
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
</form>
<aside class="card checkout-aside">
<h3>{t.checkout.shippingHintTitle}</h3>
<p class="small">{t.checkout.shippingHint}</p>
<a class="small" href="/en/versand-zahlung/">{t.checkout.shippingLink}</a>
</aside>
</div>
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/en/checkout/danke/", checkoutLang: lang }}>
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const summary = document.getElementById("checkout-summary");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
if (cart.length === 0) {
alert(emptyCartAlert);
return;
}
location.href = thankYouPath;
});
</script>
+26
View File
@@ -0,0 +1,26 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="FAQ" description="Frequently asked questions about Van's DIY & Bastelbedarf." lang={lang} path="/faq/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">FAQ</span>
<h1>{t.faq.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container faq-list">
{t.faq.items.map((f) => (
<details class="card faq-item">
<summary>{f.q}</summary>
<p class="small">{f.a}</p>
</details>
))}
</div>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
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";
const lang: Locale = "en";
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);
---
<Layout title="Home" lang={lang} path="/">
<section class="hero">
<img src="/logo.png" alt="" aria-hidden="true" class="hero-watermark" />
<div class="container hero-content">
<div class="hero-gallery">
<div class="hero-photo hero-photo-1"><img src="/img/hero-plushie-bunny.webp" alt="Crocheted bunny plushie in blue dungarees, in the grass" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-2"><img src="/img/hero-plushie-turtle.webp" alt="Crocheted turtle plushie in light blue and yellow" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-3"><img src="/img/hero-plushie-octopus.webp" alt="Crocheted octopus plushie in pink" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-4"><img src="/img/hero-plushie-penguin.webp" alt="Crocheted penguin plushie in light blue and white" loading="eager" width="900" height="1200" /></div>
</div>
<div class="hero-inner">
<span class="eyebrow">🩵 Van's DIY & Bastelbedarf</span>
<h1>Handmade with love <br />creativity to gift<br />and to keep.</h1>
<p class="lead">Crochet, plushies, jewelry and craft supplies every piece made by hand, with heart and patience.</p>
<div class="btn-row">
<a class="btn btn-primary" href="/en/shop/">Browse now</a>
<a class="btn btn-outline" href="/en/ueber-die-kreative/">About the maker</a>
</div>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">Explore categories</h2>
<div class="grid grid-3" style="margin-top:1.5rem;">
{categories.map((c) => (
<a class="card category-card" href={`/en/shop/${c.slug}/`}>
<span class="cat-icon" aria-hidden="true">{c.icon}</span>
<h3>{c.name[lang]}</h3>
<p class="small">{c.description[lang]}</p>
</a>
))}
</div>
</div>
</section>
{neuheiten.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>New arrivals</h2>
<a class="small" href="/en/shop/">View all →</a>
</div>
<div class="grid grid-4">{neuheiten.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{bestseller.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Bestsellers</h2>
<a class="small" href="/en/shop/">View all →</a>
</div>
<div class="grid grid-4">{bestseller.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{sale.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Sale %</h2>
<a class="small" href="/en/sale/">View all offers →</a>
</div>
<div class="grid grid-4">{sale.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section">
<div class="container grid grid-2 creator-section">
<div class="portrait-frame creator-photo">
<img src="/img/vanvan-portrait.png" alt="Portrait photo of VanVan" width="640" height="640" loading="lazy" />
<span class="portrait-badge">🩵 Handmade</span>
</div>
<div>
<span class="eyebrow">The maker behind the shop</span>
<h2>Hi, I'm VanVan 🩵</h2>
<blockquote class="pull-quote">"Every stitch tells its own story because true handmade craft isn't made with yarn alone, but above all with a whole lot of heart."</blockquote>
<a class="btn btn-outline" href="/en/ueber-die-kreative/">More about me →</a>
</div>
</div>
</section>
</Layout>
+66
View File
@@ -0,0 +1,66 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="Contact" description="Contact Van's DIY & Bastelbedarf." lang={lang} path="/kontakt/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.nav.contact}</span>
<h1>{t.contact.title}</h1>
<p class="lead">{t.contact.lead}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 contact-layout">
<form class="frm card" id="contact-form">
<div>
<label for="name">{t.contact.name}</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">{t.contact.email}</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="betreff">{t.contact.subject}</label>
<input id="betreff" name="betreff" type="text" />
</div>
<div>
<label for="nachricht">{t.contact.message}</label>
<textarea id="nachricht" name="nachricht" rows="5" required></textarea>
</div>
<div class="checkbox-row">
<input id="dsgvo" type="checkbox" required />
<label for="dsgvo">{t.contact.gdprPrefix} <a href="/datenschutz/">{t.contact.gdprLink}</a> {t.contact.gdprSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.contact.send}</button>
<p class="small" id="form-status" role="status"></p>
<p class="todo-note">{t.contact.todoNote}</p>
</form>
<div>
<h3>{t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:[email protected]">[email protected]</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3>{t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ sentDemo: t.contact.sentDemo }}>
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", (e) => {
e.preventDefault();
status.textContent = sentDemo;
status.style.color = "var(--c-accent)";
form.reset();
});
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="My Account" description="Guest checkout or customer account at Van's DIY & Bastelbedarf." lang={lang} path="/konto/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.account.eyebrow}</span>
<h1>{t.account.title}</h1>
<p class="todo-note">{t.account.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 account-grid">
<form class="frm card">
<h3>{t.account.loginTitle}</h3>
<div><label for="login-email">{t.account.email}</label><input id="login-email" type="email" /></div>
<div><label for="login-pw">{t.account.password}</label><input id="login-pw" type="password" /></div>
<button class="btn btn-primary btn-block" type="button" disabled>{t.account.loginButton}</button>
<a class="small" href="#">{t.account.forgotPassword}</a>
</form>
<div class="card">
<h3>{t.account.newTitle}</h3>
<p class="small">{t.account.newText}</p>
<a class="btn btn-outline btn-block" href="/en/shop/">{t.account.guestButton}</a>
</div>
</div>
</section>
</Layout>
+121
View File
@@ -0,0 +1,121 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { products, getProduct, productsByCategory } from "../../../data/products";
import { getCategory } from "../../../data/categories";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
export function getStaticPaths() {
return products.map((p) => ({ params: { slug: p.slug } }));
}
const lang: Locale = "en";
const t = useTranslations(lang);
const { slug } = Astro.params;
const product = getProduct(slug!)!;
const category = getCategory(product.kategorie);
const empfehlungen = productsByCategory(product.kategorie).filter((p) => p.slug !== product.slug).slice(0, 3);
const name = product.name[lang];
const desc = product.beschreibung[lang];
const badgeLabel: Record<string, string> = { neu: t.badge.neu, bestseller: t.badge.bestseller, sale: t.badge.sale, handgemacht: t.badge.handgemacht };
const felder: [string, string | undefined][] = [
[t.product.articleNo, product.artikelnummer],
[t.product.material, product.material],
[t.product.size, product.groesse],
[t.product.colors, product.farben?.join(", ")],
[t.product.included, product.lieferumfang],
[t.product.care, product.pflegehinweise],
[t.product.delivery, product.lieferzeit],
];
---
<Layout title={name} description={desc} lang={lang} path={`/produkt/${product.slug}/`}>
<section class="section-tight">
<div class="container">
<p class="small breadcrumb">
<a href="/en/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/en/shop/${product.kategorie}/`}>{category?.name[lang]}</a> / {name}
</p>
<div class="grid grid-2 product-detail">
<div>
<div class="img-placeholder product-hero" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
{t.common.photoSoon}<br /><span class="small">{t.product.photoNote}</span>
</div>
<div class="thumb-row">
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
</div>
</div>
<div>
<div class="badge-row">
{product.badges.map((b) => <span class={`badge ${b === "sale" ? "badge-sale" : ""}`}>{badgeLabel[b]}</span>)}
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
</div>
<h1>{name}</h1>
<div class="price-row-detail">
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
<span class="price-detail">{formatPrice(product.preis, lang)}</span>
</div>
<p class="small">{t.common.vatNote} · {t.common.plusShipping}</p>
<p class="lead">{desc}</p>
<div class="qty-row">
<label for="qty">{t.common.quantity}</label>
<input type="number" id="qty" min="1" value="1" />
<button
class="btn btn-primary"
id="add-to-cart"
disabled={product.bestand === 0}
data-slug={product.slug}
data-name={name}
data-preis={product.preis}
>
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
</button>
</div>
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
{felder.some(([, v]) => v) && (
<table class="specs">
<tbody>
{felder.filter(([, v]) => v).map(([k, v]) => (
<tr><th>{k}</th><td>{v}</td></tr>
))}
</tbody>
</table>
)}
<div class="todo-note">{t.product.todoFields}</div>
</div>
</div>
</div>
</section>
{empfehlungen.length > 0 && (
<section class="section-tight">
<div class="container">
<h2>{t.product.recommendations}</h2>
<div class="grid grid-3">{empfehlungen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
</Layout>
<script>
import { addToCart } from "../../../scripts/cart";
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
const qtyInput = document.getElementById("qty") as HTMLInputElement;
const confirm = document.getElementById("add-confirm")!;
btn?.addEventListener("click", () => {
const { slug, name, preis } = btn.dataset;
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
confirm.style.display = "block";
setTimeout(() => (confirm.style.display = "none"), 2500);
});
</script>
+31
View File
@@ -0,0 +1,31 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { saleProducts } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
const items = saleProducts();
---
<Layout title="Sale" description="Current offers at Van's DIY & Bastelbedarf." lang={lang} path="/sale/">
<div class="container">
<section class="page-banner" style="background-image:url('/img/banner-sale.webp'); --banner-ratio: 1600 / 639;">
<div class="page-banner-content">
<span class="eyebrow">{t.sale.eyebrow}</span>
<h1>{t.sale.title}</h1>
<p class="lead">{t.sale.lead}</p>
</div>
</section>
</div>
<section class="section-tight">
<div class="container">
{items.length > 0 ? (
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
) : (
<p class="small">{t.sale.empty}</p>
)}
</div>
</section>
</Layout>
+49
View File
@@ -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 = "en";
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="/en/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={`/en/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 = "en";
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]} at 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="/en/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/en/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={`/en/shop/${category.slug}/`}>← {category.name[lang]}</a>
<a class="btn btn-primary" href="/en/shop/">{t.common.toShop}</a>
</div>
</div>
</section>
</Layout>
+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 = "en";
const t = useTranslations(lang);
---
<Layout title="Shop" description="The full Van's DIY & Bastelbedarf shop crochet, plushies, jewelry and craft supplies." 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>
@@ -0,0 +1,83 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="About the Maker" description="Get to know VanVan the person behind Van's DIY & Bastelbedarf." lang={lang} path="/ueber-die-kreative/">
<section class="section-tight">
<div class="container grid grid-2 intro">
<div class="portrait-frame portrait">
<img src="/img/vanvan-portrait-holding.webp" alt="VanVan in her workshop with her handmade crochet plushies" width="900" height="1353" loading="lazy" />
<span class="portrait-badge">{t.common.handmadeBadge}</span>
</div>
<div>
<span class="eyebrow">{t.about.eyebrow}</span>
<h1>{t.about.title}</h1>
<p class="lead">{t.about.lead}</p>
</div>
</div>
</section>
<section class="section-tight">
<div class="container story">
{t.about.story.map((s) => (
<div class="story-section">
<h2>{s.title}</h2>
{s.paragraphs.map((p) => <p>{p}</p>)}
</div>
))}
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">{t.about.valuesTitle}</h2>
<div class="grid grid-3">
{t.about.values.map((v) => (
<div class="card text-center">
<h3>{v.icon} {v.title}</h3>
<p class="small">{v.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<blockquote class="pull-quote pull-quote-center">"{t.about.closing}"</blockquote>
</div>
</section>
<section class="section-tight">
<div class="container text-center">
<a class="btn btn-primary" href="/en/shop/">{t.common.toShop}</a>
</div>
</section>
</Layout>
<script>
// Dezente Scroll-Animation für die Story-Abschnitte (Wunsch aus Dokument1.pdf: "sanfte
// Scroll-Animationen sind willkommen"). Fällt sauber zurück, falls IntersectionObserver
// fehlt oder der Nutzer reduzierte Bewegung bevorzugt (siehe CSS prefers-reduced-motion).
const sections = document.querySelectorAll(".story-section");
if ("IntersectionObserver" in window && sections.length) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.15 }
);
sections.forEach((el) => io.observe(el));
} else {
sections.forEach((el) => el.classList.add("is-visible"));
}
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="Shipping & Payment" description="Shipping terms and payment methods at Van's DIY & Bastelbedarf." lang={lang} path="/versand-zahlung/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.shipping.eyebrow}</span>
<h1>{t.shipping.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2">
<div class="card">
<h3>{t.shipping.shippingTitle}</h3>
<ul>
{t.shipping.shippingItems.map((item) => <li>{item}</li>)}
</ul>
</div>
<div class="card">
<h3>{t.shipping.paymentTitle}</h3>
<ul>
{t.shipping.paymentItems.map((item) => <li>{item}</li>)}
</ul>
<p class="small">{t.common.vatNote}</p>
</div>
</div>
<p class="todo-note">{t.shipping.todoNote}</p>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "en";
const t = useTranslations(lang);
---
<Layout title="Cart" description="Your cart at Van's DIY & Bastelbedarf." lang={lang} path="/warenkorb/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.cart.eyebrow}</span>
<h1>{t.cart.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container">
<div id="cart-empty" class="card" style="display:none;">
<p>{t.cart.empty}</p>
<a class="btn btn-primary" href="/en/shop/">{t.common.browseShop}</a>
</div>
<div id="cart-content" class="cart-layout" style="display:none;">
<div class="card cart-items" id="cart-items"></div>
<aside class="card cart-summary">
<h3>{t.cart.subtotal}</h3>
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
<div class="summary-row"><span>{t.cart.shipping}</span><span id="sum-shipping">{t.cart.shippingCalculated}</span></div>
<p class="small" id="shipping-hint"></p>
<hr class="divider" />
<div class="summary-row total"><span>{t.cart.total}</span><span id="sum-total">0,00 €</span></div>
<p class="small">{t.common.vatNote}</p>
<a class="btn btn-primary btn-block" href="/en/checkout/">{t.cart.toCheckout}</a>
</aside>
</div>
</div>
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
const content = document.getElementById("cart-content");
const itemsEl = document.getElementById("cart-items");
if (cart.length === 0) {
empty.style.display = "block";
content.style.display = "none";
return;
}
empty.style.display = "none";
content.style.display = "grid";
itemsEl.innerHTML = cart.map((i) => `
<div class="cart-item">
<div class="img-placeholder thumb"></div>
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(i.preis, cartLang)} ${perItemLabel}</div>
</div>
<div class="qty-controls">
<button data-action="dec" data-slug="${i.slug}"></button>
<span>${i.menge}</span>
<button data-action="inc" data-slug="${i.slug}">+</button>
</div>
<div><strong>${formatPrice(i.preis * i.menge, cartLang)}</strong></div>
<a href="#" class="remove" data-action="remove" data-slug="${i.slug}">${removeLabel}</a>
</div>
`).join("");
const subtotal = cartTotal();
const remaining = Math.max(0, freeShipFrom - subtotal);
document.getElementById("sum-subtotal").textContent = formatPrice(subtotal, cartLang);
document.getElementById("sum-total").textContent = formatPrice(subtotal, cartLang);
document.getElementById("shipping-hint").textContent =
remaining > 0 ? remainingTemplate.replace("__V__", formatPrice(remaining, cartLang)) : freeShippingText;
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const target = e.currentTarget;
const slug = target.dataset.slug;
const action = target.dataset.action;
const item = getCart().find((i) => i.slug === slug);
if (!item) return;
if (action === "inc") updateQuantity(slug, item.menge + 1);
if (action === "dec") updateQuantity(slug, item.menge - 1);
if (action === "remove") removeFromCart(slug);
render();
});
});
}
render();
window.addEventListener("cart:changed", render);
</script>
+26
View File
@@ -0,0 +1,26 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="FAQ" description="Häufig gestellte Fragen zu Van's DIY & Bastelbedarf." lang={lang} path="/faq/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">FAQ</span>
<h1>{t.faq.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container faq-list">
{t.faq.items.map((f) => (
<details class="card faq-item">
<summary>{f.q}</summary>
<p class="small">{f.a}</p>
</details>
))}
</div>
</section>
</Layout>
+23
View File
@@ -0,0 +1,23 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title={t.checkout.thankYouTitle} lang={lang} path="/checkout/danke/">
<section class="section">
<div class="container text-center danke">
<span class="eyebrow">🩵 {t.checkout.thankYouEyebrow}!</span>
<h1>{t.checkout.thankYouTitle}</h1>
<p class="lead">{t.checkout.thankYouLead}</p>
<a class="btn btn-primary" href="/fr/shop/">{t.checkout.continueShopping}</a>
</div>
</section>
</Layout>
<script>
localStorage.removeItem("vandiy_cart_v1");
window.dispatchEvent(new CustomEvent("cart:changed"));
</script>
+99
View File
@@ -0,0 +1,99 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Paiement" description="Finaliser la commande chez Van's DIY & Bastelbedarf." lang={lang} path="/checkout/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.checkout.eyebrow}</span>
<h1>{t.checkout.title}</h1>
<p class="todo-note">{t.checkout.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container checkout-layout">
<form class="frm card" id="checkout-form">
<h3>{t.checkout.step1}</h3>
<div><label for="email">{t.checkout.email}</label><input id="email" type="email" required /></div>
<div class="grid grid-2">
<div><label for="vorname">{t.checkout.firstName}</label><input id="vorname" type="text" required /></div>
<div><label for="nachname">{t.checkout.lastName}</label><input id="nachname" type="text" required /></div>
</div>
<div><label for="strasse">{t.checkout.street}</label><input id="strasse" type="text" placeholder="1 Rue de lExemple" required /></div>
<div class="grid grid-2">
<div><label for="plz">{t.checkout.zip}</label><input id="plz" type="text" inputmode="numeric" maxlength="5" placeholder="75001" required /></div>
<div><label for="ort">{t.checkout.city}</label><input id="ort" type="text" placeholder="Paris" required /></div>
</div>
<div>
<label for="land">{t.checkout.country}</label>
<select id="land">
<option>Allemagne</option>
<option>Autriche</option>
<option>Suisse</option>
<option>Luxembourg</option>
</select>
</div>
<hr class="divider" />
<h3>{t.checkout.step2}</h3>
<div class="payment-options">
<label class="pay-option"><input type="radio" name="pay" value="paypal" checked /> PayPal</label>
<label class="pay-option"><input type="radio" name="pay" value="klarna" /> Klarna</label>
<label class="pay-option"><input type="radio" name="pay" value="ueberweisung" /> {t.checkout.bankTransfer}</label>
</div>
<hr class="divider" />
<h3>{t.checkout.step3}</h3>
<div id="checkout-summary" class="checkout-summary"></div>
<p class="small">{t.common.vatNote}</p>
<div class="checkbox-row">
<input id="agb" type="checkbox" required />
<label for="agb">{t.checkout.agbPrefix} <a href="/agb/">{t.checkout.agbLink}</a> {t.checkout.agbSuffix}</label>
</div>
<div class="checkbox-row">
<input id="widerruf" type="checkbox" required />
<label for="widerruf">{t.checkout.revocationPrefix} <a href="/widerruf/">{t.checkout.revocationLink}</a> {t.checkout.revocationAnd} <a href="/datenschutz/">{t.checkout.privacyLink}</a> {t.checkout.revocationSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.checkout.orderButton}</button>
</form>
<aside class="card checkout-aside">
<h3>{t.checkout.shippingHintTitle}</h3>
<p class="small">{t.checkout.shippingHint}</p>
<a class="small" href="/fr/versand-zahlung/">{t.checkout.shippingLink}</a>
</aside>
</div>
</section>
</Layout>
<script define:vars={{ freeLabel: t.checkout.free, totalLabel: t.checkout.total, shippingLabel: t.cart.shipping, emptyCartAlert: t.checkout.emptyCartAlert, thankYouPath: "/fr/checkout/danke/", checkoutLang: lang }}>
import { getCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
const summary = document.getElementById("checkout-summary");
const cart = getCart();
const subtotal = cartTotal();
const shipping = cart.length === 0 ? 0 : subtotal >= 75 ? 0 : 4.95;
summary.innerHTML = `
${cart.map((i) => `<div class="row"><span>${i.menge}× ${i.name}</span><span>${formatPrice(i.preis * i.menge, checkoutLang)}</span></div>`).join("")}
<div class="row"><span>${shippingLabel}</span><span>${shipping === 0 ? freeLabel : formatPrice(shipping, checkoutLang)}</span></div>
<div class="row total"><span>${totalLabel}</span><span>${formatPrice(subtotal + shipping, checkoutLang)}</span></div>
`;
document.getElementById("checkout-form").addEventListener("submit", (e) => {
e.preventDefault();
if (cart.length === 0) {
alert(emptyCartAlert);
return;
}
location.href = thankYouPath;
});
</script>
+26
View File
@@ -0,0 +1,26 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="FAQ" description="Questions fréquentes sur Van's DIY & Bastelbedarf." lang={lang} path="/faq/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">FAQ</span>
<h1>{t.faq.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container faq-list">
{t.faq.items.map((f) => (
<details class="card faq-item">
<summary>{f.q}</summary>
<p class="small">{f.a}</p>
</details>
))}
</div>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
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";
const lang: Locale = "fr";
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);
---
<Layout title="Accueil" lang={lang} path="/">
<section class="hero">
<img src="/logo.png" alt="" aria-hidden="true" class="hero-watermark" />
<div class="container hero-content">
<div class="hero-gallery">
<div class="hero-photo hero-photo-1"><img src="/img/hero-plushie-bunny.webp" alt="Peluche lapin au crochet en salopette bleue, dans l'herbe" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-2"><img src="/img/hero-plushie-turtle.webp" alt="Peluche tortue au crochet en bleu clair et jaune" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-3"><img src="/img/hero-plushie-octopus.webp" alt="Peluche poulpe au crochet en rose" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-4"><img src="/img/hero-plushie-penguin.webp" alt="Peluche pingouin au crochet en bleu clair et blanc" loading="eager" width="900" height="1200" /></div>
</div>
<div class="hero-inner">
<span class="eyebrow">🩵 Van's DIY & Bastelbedarf</span>
<h1>Fait main avec amour <br />de la créativité à offrir<br />ou à garder.</h1>
<p class="lead">Crochet, peluches, bijoux et fournitures créatives chaque pièce façonnée à la main, avec cœur et patience.</p>
<div class="btn-row">
<a class="btn btn-primary" href="/fr/shop/">Découvrir la boutique</a>
<a class="btn btn-outline" href="/fr/ueber-die-kreative/">À propos de la créatrice</a>
</div>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">Découvrir les catégories</h2>
<div class="grid grid-3" style="margin-top:1.5rem;">
{categories.map((c) => (
<a class="card category-card" href={`/fr/shop/${c.slug}/`}>
<span class="cat-icon" aria-hidden="true">{c.icon}</span>
<h3>{c.name[lang]}</h3>
<p class="small">{c.description[lang]}</p>
</a>
))}
</div>
</div>
</section>
{neuheiten.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Nouveautés</h2>
<a class="small" href="/fr/shop/">Tout voir →</a>
</div>
<div class="grid grid-4">{neuheiten.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{bestseller.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Best-sellers</h2>
<a class="small" href="/fr/shop/">Tout voir →</a>
</div>
<div class="grid grid-4">{bestseller.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{sale.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Soldes %</h2>
<a class="small" href="/fr/sale/">Toutes les offres →</a>
</div>
<div class="grid grid-4">{sale.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section">
<div class="container grid grid-2 creator-section">
<div class="portrait-frame creator-photo">
<img src="/img/vanvan-portrait.png" alt="Photo de VanVan" width="640" height="640" loading="lazy" />
<span class="portrait-badge">🩵 Fait main</span>
</div>
<div>
<span class="eyebrow">La créatrice derrière la boutique</span>
<h2>Bonjour, je suis VanVan 🩵</h2>
<blockquote class="pull-quote">« Chaque maille raconte sa propre histoire car le véritable artisanat ne se crée pas seulement avec de la laine, mais surtout avec beaucoup de cœur. »</blockquote>
<a class="btn btn-outline" href="/fr/ueber-die-kreative/">En savoir plus sur moi →</a>
</div>
</div>
</section>
</Layout>
+66
View File
@@ -0,0 +1,66 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Contact" description="Contactez Van's DIY & Bastelbedarf." lang={lang} path="/kontakt/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.nav.contact}</span>
<h1>{t.contact.title}</h1>
<p class="lead">{t.contact.lead}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 contact-layout">
<form class="frm card" id="contact-form">
<div>
<label for="name">{t.contact.name}</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">{t.contact.email}</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="betreff">{t.contact.subject}</label>
<input id="betreff" name="betreff" type="text" />
</div>
<div>
<label for="nachricht">{t.contact.message}</label>
<textarea id="nachricht" name="nachricht" rows="5" required></textarea>
</div>
<div class="checkbox-row">
<input id="dsgvo" type="checkbox" required />
<label for="dsgvo">{t.contact.gdprPrefix} <a href="/datenschutz/">{t.contact.gdprLink}</a> {t.contact.gdprSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.contact.send}</button>
<p class="small" id="form-status" role="status"></p>
<p class="todo-note">{t.contact.todoNote}</p>
</form>
<div>
<h3>{t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:[email protected]">[email protected]</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3>{t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ sentDemo: t.contact.sentDemo }}>
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", (e) => {
e.preventDefault();
status.textContent = sentDemo;
status.style.color = "var(--c-accent)";
form.reset();
});
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Mon compte" description="Commande invité ou compte client chez Van's DIY & Bastelbedarf." lang={lang} path="/konto/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.account.eyebrow}</span>
<h1>{t.account.title}</h1>
<p class="todo-note">{t.account.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 account-grid">
<form class="frm card">
<h3>{t.account.loginTitle}</h3>
<div><label for="login-email">{t.account.email}</label><input id="login-email" type="email" /></div>
<div><label for="login-pw">{t.account.password}</label><input id="login-pw" type="password" /></div>
<button class="btn btn-primary btn-block" type="button" disabled>{t.account.loginButton}</button>
<a class="small" href="#">{t.account.forgotPassword}</a>
</form>
<div class="card">
<h3>{t.account.newTitle}</h3>
<p class="small">{t.account.newText}</p>
<a class="btn btn-outline btn-block" href="/fr/shop/">{t.account.guestButton}</a>
</div>
</div>
</section>
</Layout>
+121
View File
@@ -0,0 +1,121 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { products, getProduct, productsByCategory } from "../../../data/products";
import { getCategory } from "../../../data/categories";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
import { formatPrice } from "../../../i18n/format";
export function getStaticPaths() {
return products.map((p) => ({ params: { slug: p.slug } }));
}
const lang: Locale = "fr";
const t = useTranslations(lang);
const { slug } = Astro.params;
const product = getProduct(slug!)!;
const category = getCategory(product.kategorie);
const empfehlungen = productsByCategory(product.kategorie).filter((p) => p.slug !== product.slug).slice(0, 3);
const name = product.name[lang];
const desc = product.beschreibung[lang];
const badgeLabel: Record<string, string> = { neu: t.badge.neu, bestseller: t.badge.bestseller, sale: t.badge.sale, handgemacht: t.badge.handgemacht };
const felder: [string, string | undefined][] = [
[t.product.articleNo, product.artikelnummer],
[t.product.material, product.material],
[t.product.size, product.groesse],
[t.product.colors, product.farben?.join(", ")],
[t.product.included, product.lieferumfang],
[t.product.care, product.pflegehinweise],
[t.product.delivery, product.lieferzeit],
];
---
<Layout title={name} description={desc} lang={lang} path={`/produkt/${product.slug}/`}>
<section class="section-tight">
<div class="container">
<p class="small breadcrumb">
<a href="/fr/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/fr/shop/${product.kategorie}/`}>{category?.name[lang]}</a> / {name}
</p>
<div class="grid grid-2 product-detail">
<div>
<div class="img-placeholder product-hero" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
{t.common.photoSoon}<br /><span class="small">{t.product.photoNote}</span>
</div>
<div class="thumb-row">
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
</div>
</div>
<div>
<div class="badge-row">
{product.badges.map((b) => <span class={`badge ${b === "sale" ? "badge-sale" : ""}`}>{badgeLabel[b]}</span>)}
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
</div>
<h1>{name}</h1>
<div class="price-row-detail">
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
<span class="price-detail">{formatPrice(product.preis, lang)}</span>
</div>
<p class="small">{t.common.vatNote} · {t.common.plusShipping}</p>
<p class="lead">{desc}</p>
<div class="qty-row">
<label for="qty">{t.common.quantity}</label>
<input type="number" id="qty" min="1" value="1" />
<button
class="btn btn-primary"
id="add-to-cart"
disabled={product.bestand === 0}
data-slug={product.slug}
data-name={name}
data-preis={product.preis}
>
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
</button>
</div>
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
{felder.some(([, v]) => v) && (
<table class="specs">
<tbody>
{felder.filter(([, v]) => v).map(([k, v]) => (
<tr><th>{k}</th><td>{v}</td></tr>
))}
</tbody>
</table>
)}
<div class="todo-note">{t.product.todoFields}</div>
</div>
</div>
</div>
</section>
{empfehlungen.length > 0 && (
<section class="section-tight">
<div class="container">
<h2>{t.product.recommendations}</h2>
<div class="grid grid-3">{empfehlungen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
</Layout>
<script>
import { addToCart } from "../../../scripts/cart";
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
const qtyInput = document.getElementById("qty") as HTMLInputElement;
const confirm = document.getElementById("add-confirm")!;
btn?.addEventListener("click", () => {
const { slug, name, preis } = btn.dataset;
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
confirm.style.display = "block";
setTimeout(() => (confirm.style.display = "none"), 2500);
});
</script>
+31
View File
@@ -0,0 +1,31 @@
---
import Layout from "../../../layouts/Layout.astro";
import ProductCard from "../../../components/ProductCard.astro";
import { saleProducts } from "../../../data/products";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
const items = saleProducts();
---
<Layout title="Soldes" description="Offres actuelles chez Van's DIY & Bastelbedarf." lang={lang} path="/sale/">
<div class="container">
<section class="page-banner" style="background-image:url('/img/banner-sale.webp'); --banner-ratio: 1600 / 639;">
<div class="page-banner-content">
<span class="eyebrow">{t.sale.eyebrow}</span>
<h1>{t.sale.title}</h1>
<p class="lead">{t.sale.lead}</p>
</div>
</section>
</div>
<section class="section-tight">
<div class="container">
{items.length > 0 ? (
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
) : (
<p class="small">{t.sale.empty}</p>
)}
</div>
</section>
</Layout>
+49
View File
@@ -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 = "fr";
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="/fr/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={`/fr/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 = "fr";
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]} chez 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="/fr/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/fr/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={`/fr/shop/${category.slug}/`}>← {category.name[lang]}</a>
<a class="btn btn-primary" href="/fr/shop/">{t.common.toShop}</a>
</div>
</div>
</section>
</Layout>
+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>
@@ -0,0 +1,83 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="À propos de la créatrice" description="Découvrez VanVan la personne derrière Van's DIY & Bastelbedarf." lang={lang} path="/ueber-die-kreative/">
<section class="section-tight">
<div class="container grid grid-2 intro">
<div class="portrait-frame portrait">
<img src="/img/vanvan-portrait-holding.webp" alt="VanVan dans son atelier avec ses peluches au crochet faites main" width="900" height="1353" loading="lazy" />
<span class="portrait-badge">{t.common.handmadeBadge}</span>
</div>
<div>
<span class="eyebrow">{t.about.eyebrow}</span>
<h1>{t.about.title}</h1>
<p class="lead">{t.about.lead}</p>
</div>
</div>
</section>
<section class="section-tight">
<div class="container story">
{t.about.story.map((s) => (
<div class="story-section">
<h2>{s.title}</h2>
{s.paragraphs.map((p) => <p>{p}</p>)}
</div>
))}
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">{t.about.valuesTitle}</h2>
<div class="grid grid-3">
{t.about.values.map((v) => (
<div class="card text-center">
<h3>{v.icon} {v.title}</h3>
<p class="small">{v.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<blockquote class="pull-quote pull-quote-center">« {t.about.closing} »</blockquote>
</div>
</section>
<section class="section-tight">
<div class="container text-center">
<a class="btn btn-primary" href="/fr/shop/">{t.common.toShop}</a>
</div>
</section>
</Layout>
<script>
// Dezente Scroll-Animation für die Story-Abschnitte (Wunsch aus Dokument1.pdf: "sanfte
// Scroll-Animationen sind willkommen"). Fällt sauber zurück, falls IntersectionObserver
// fehlt oder der Nutzer reduzierte Bewegung bevorzugt (siehe CSS prefers-reduced-motion).
const sections = document.querySelectorAll(".story-section");
if ("IntersectionObserver" in window && sections.length) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.15 }
);
sections.forEach((el) => io.observe(el));
} else {
sections.forEach((el) => el.classList.add("is-visible"));
}
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Livraison & Paiement" description="Conditions de livraison et moyens de paiement chez Van's DIY & Bastelbedarf." lang={lang} path="/versand-zahlung/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.shipping.eyebrow}</span>
<h1>{t.shipping.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2">
<div class="card">
<h3>{t.shipping.shippingTitle}</h3>
<ul>
{t.shipping.shippingItems.map((item) => <li>{item}</li>)}
</ul>
</div>
<div class="card">
<h3>{t.shipping.paymentTitle}</h3>
<ul>
{t.shipping.paymentItems.map((item) => <li>{item}</li>)}
</ul>
<p class="small">{t.common.vatNote}</p>
</div>
</div>
<p class="todo-note">{t.shipping.todoNote}</p>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
import Layout from "../../../layouts/Layout.astro";
import type { Locale } from "../../../i18n/config";
import { useTranslations } from "../../../i18n/ui";
const lang: Locale = "fr";
const t = useTranslations(lang);
---
<Layout title="Panier" description="Votre panier chez Van's DIY & Bastelbedarf." lang={lang} path="/warenkorb/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.cart.eyebrow}</span>
<h1>{t.cart.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container">
<div id="cart-empty" class="card" style="display:none;">
<p>{t.cart.empty}</p>
<a class="btn btn-primary" href="/fr/shop/">{t.common.browseShop}</a>
</div>
<div id="cart-content" class="cart-layout" style="display:none;">
<div class="card cart-items" id="cart-items"></div>
<aside class="card cart-summary">
<h3>{t.cart.subtotal}</h3>
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
<div class="summary-row"><span>{t.cart.shipping}</span><span id="sum-shipping">{t.cart.shippingCalculated}</span></div>
<p class="small" id="shipping-hint"></p>
<hr class="divider" />
<div class="summary-row total"><span>{t.cart.total}</span><span id="sum-total">0,00 €</span></div>
<p class="small">{t.common.vatNote}</p>
<a class="btn btn-primary btn-block" href="/fr/checkout/">{t.cart.toCheckout}</a>
</aside>
</div>
</div>
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../../scripts/cart";
import { formatPrice } from "../../../i18n/format";
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
const content = document.getElementById("cart-content");
const itemsEl = document.getElementById("cart-items");
if (cart.length === 0) {
empty.style.display = "block";
content.style.display = "none";
return;
}
empty.style.display = "none";
content.style.display = "grid";
itemsEl.innerHTML = cart.map((i) => `
<div class="cart-item">
<div class="img-placeholder thumb"></div>
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(i.preis, cartLang)} ${perItemLabel}</div>
</div>
<div class="qty-controls">
<button data-action="dec" data-slug="${i.slug}"></button>
<span>${i.menge}</span>
<button data-action="inc" data-slug="${i.slug}">+</button>
</div>
<div><strong>${formatPrice(i.preis * i.menge, cartLang)}</strong></div>
<a href="#" class="remove" data-action="remove" data-slug="${i.slug}">${removeLabel}</a>
</div>
`).join("");
const subtotal = cartTotal();
const remaining = Math.max(0, freeShipFrom - subtotal);
document.getElementById("sum-subtotal").textContent = formatPrice(subtotal, cartLang);
document.getElementById("sum-total").textContent = formatPrice(subtotal, cartLang);
document.getElementById("shipping-hint").textContent =
remaining > 0 ? remainingTemplate.replace("__V__", formatPrice(remaining, cartLang)) : freeShippingText;
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const target = e.currentTarget;
const slug = target.dataset.slug;
const action = target.dataset.action;
const item = getCart().find((i) => i.slug === slug);
if (!item) return;
if (action === "inc") updateQuantity(slug, item.menge + 1);
if (action === "dec") updateQuantity(slug, item.menge - 1);
if (action === "remove") removeFromCart(slug);
render();
});
});
}
render();
window.addEventListener("cart:changed", render);
</script>
+51
View File
@@ -0,0 +1,51 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="Impressum" path="/impressum/" legalOnly={true}>
<section class="section-tight">
<div class="container">
<h1>Impressum</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Dies ist ein vorbereiteter Platzhalter gemäß Pflichtenheft
und ersetzt keine rechtliche Prüfung. Vor Veröffentlichung bitte von einer fachkundigen
Stelle (z. B. Anwalt/Anwältin oder IHK-Impressumsgenerator) prüfen und mit den echten
Angaben von VanVan vervollständigen.
</div>
<h2>Angaben gemäß § 5 TMG</h2>
<p>
[Vorname Nachname] („VanVan")<br />
Van's DIY & Bastelbedarf<br />
Einzelunternehmen<br />
[Straße und Hausnummer]<br />
[PLZ Ort]<br />
Deutschland
</p>
<h2>Kontakt</h2>
<p>
Telefon: [Telefonnummer]<br />
E-Mail: [E-Mail-Adresse]
</p>
<h2>Umsatzsteuer</h2>
<p>Gemäß §19 UStG wird als Kleinunternehmer keine Umsatzsteuer berechnet und ausgewiesen.</p>
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p>[Vorname Nachname], Anschrift wie oben.</p>
<h2>EU-Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a>.
Unsere E-Mail-Adresse finden Sie oben im Impressum.
</p>
<h2>Verbraucherstreitbeilegung/Universalschlichtungsstelle</h2>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen. [Ggf. anpassen, falls sich das ändert.]
</p>
</div>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
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";
const lang: Locale = "de";
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);
---
<Layout title="Startseite" lang={lang} path="/">
<section class="hero">
<img src="/logo.png" alt="" aria-hidden="true" class="hero-watermark" />
<div class="container hero-content">
<div class="hero-gallery">
<div class="hero-photo hero-photo-1"><img src="/img/hero-plushie-bunny.webp" alt="Gehäkeltes Hasen-Plushie in blauer Latzhose, im Gras" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-2"><img src="/img/hero-plushie-turtle.webp" alt="Gehäkeltes Schildkröten-Plushie in Hellblau und Gelb" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-3"><img src="/img/hero-plushie-octopus.webp" alt="Gehäkeltes Oktopus-Plushie in Rosa" loading="eager" width="900" height="1200" /></div>
<div class="hero-photo hero-photo-4"><img src="/img/hero-plushie-penguin.webp" alt="Gehäkeltes Pinguin-Plushie in Hellblau und Weiß" loading="eager" width="900" height="1200" /></div>
</div>
<div class="hero-inner">
<span class="eyebrow">🩵 Van's DIY & Bastelbedarf</span>
<h1>Handgemacht mit Liebe <br />Kreativität zum Verschenken<br />und Selbstbehalten.</h1>
<p class="lead">Häkelwerke, Plushies, Schmuck und Bastelzubehör jedes Stück von Hand gefertigt, mit Herz und Geduld.</p>
<div class="btn-row">
<a class="btn btn-primary" href="/shop/">Jetzt stöbern</a>
<a class="btn btn-outline" href="/ueber-die-kreative/">Über die Kreative</a>
</div>
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">Kategorien entdecken</h2>
<div class="grid grid-3" style="margin-top:1.5rem;">
{categories.map((c) => (
<a class="card category-card" href={`/shop/${c.slug}/`}>
<span class="cat-icon" aria-hidden="true">{c.icon}</span>
<h3>{c.name[lang]}</h3>
<p class="small">{c.description[lang]}</p>
</a>
))}
</div>
</div>
</section>
{neuheiten.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Neuheiten</h2>
<a class="small" href="/shop/">Alle ansehen →</a>
</div>
<div class="grid grid-4">{neuheiten.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{bestseller.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Bestseller</h2>
<a class="small" href="/shop/">Alle ansehen →</a>
</div>
<div class="grid grid-4">{bestseller.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
{sale.length > 0 && (
<section class="section-tight">
<div class="container">
<div class="section-head">
<h2>Sale %</h2>
<a class="small" href="/sale/">Alle Angebote →</a>
</div>
<div class="grid grid-4">{sale.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
<section class="section">
<div class="container grid grid-2 creator-section">
<div class="portrait-frame creator-photo">
<img src="/img/vanvan-portrait.png" alt="Porträtfoto von VanVan" width="640" height="640" loading="lazy" />
<span class="portrait-badge">🩵 Handgemacht</span>
</div>
<div>
<span class="eyebrow">Die Kreative hinter dem Shop</span>
<h2>Hallo, ich bin VanVan 🩵</h2>
<blockquote class="pull-quote">„Jede Masche erzählt ihre eigene Geschichte denn wahre Handarbeit entsteht nicht nur mit Wolle, sondern vor allem mit ganz viel Herz.”</blockquote>
<a class="btn btn-outline" href="/ueber-die-kreative/">Mehr über mich →</a>
</div>
</div>
</section>
</Layout>
+66
View File
@@ -0,0 +1,66 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Kontakt" description="Kontaktiere Van's DIY & Bastelbedarf." lang={lang} path="/kontakt/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.nav.contact}</span>
<h1>{t.contact.title}</h1>
<p class="lead">{t.contact.lead}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 contact-layout">
<form class="frm card" id="contact-form">
<div>
<label for="name">{t.contact.name}</label>
<input id="name" name="name" type="text" required />
</div>
<div>
<label for="email">{t.contact.email}</label>
<input id="email" name="email" type="email" required />
</div>
<div>
<label for="betreff">{t.contact.subject}</label>
<input id="betreff" name="betreff" type="text" />
</div>
<div>
<label for="nachricht">{t.contact.message}</label>
<textarea id="nachricht" name="nachricht" rows="5" required></textarea>
</div>
<div class="checkbox-row">
<input id="dsgvo" type="checkbox" required />
<label for="dsgvo">{t.contact.gdprPrefix} <a href="/datenschutz/">{t.contact.gdprLink}</a> {t.contact.gdprSuffix}</label>
</div>
<button class="btn btn-primary btn-block" type="submit">{t.contact.send}</button>
<p class="small" id="form-status" role="status"></p>
<p class="todo-note">{t.contact.todoNote}</p>
</form>
<div>
<h3>{t.contact.directTitle}</h3>
<p class="small">E-Mail: <a href="mailto:[email protected]">[email protected]</a> <span class="small">{t.contact.emailPlaceholderNote}</span></p>
<p class="small">{t.contact.orderNote}</p>
<h3>{t.contact.responseTitle}</h3>
<p class="small">{t.contact.responseNote}</p>
</div>
</div>
</section>
</Layout>
<script define:vars={{ sentDemo: t.contact.sentDemo }}>
const form = document.getElementById("contact-form");
const status = document.getElementById("form-status");
form.addEventListener("submit", (e) => {
e.preventDefault();
status.textContent = sentDemo;
status.style.color = "var(--c-accent)";
form.reset();
});
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Mein Konto" description="Gastbestellung oder Kundenkonto bei Van's DIY & Bastelbedarf." lang={lang} path="/konto/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.account.eyebrow}</span>
<h1>{t.account.title}</h1>
<p class="todo-note">{t.account.todoNote}</p>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2 account-grid">
<form class="frm card">
<h3>{t.account.loginTitle}</h3>
<div><label for="login-email">{t.account.email}</label><input id="login-email" type="email" /></div>
<div><label for="login-pw">{t.account.password}</label><input id="login-pw" type="password" /></div>
<button class="btn btn-primary btn-block" type="button" disabled>{t.account.loginButton}</button>
<a class="small" href="#">{t.account.forgotPassword}</a>
</form>
<div class="card">
<h3>{t.account.newTitle}</h3>
<p class="small">{t.account.newText}</p>
<a class="btn btn-outline btn-block" href="/shop/">{t.account.guestButton}</a>
</div>
</div>
</section>
</Layout>
@@ -0,0 +1,33 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="Muster-Widerrufsformular" path="/muster-widerrufsformular/" legalOnly={true}>
<section class="section-tight">
<div class="container">
<h1>Muster-Widerrufsformular</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung vor Veröffentlichung. Dieses Formular ist nicht verpflichtend — du kannst deinen
Widerruf auch formlos erklären.
</div>
<p>(Wenn du den Vertrag widerrufen möchtest, fülle bitte dieses Formular aus und sende es zurück.)</p>
<div class="card">
<p>An<br />[Vorname Nachname]<br />Van's DIY & Bastelbedarf<br />[Anschrift]<br />[E-Mail-Adresse]</p>
<p>
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf
der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
</p>
<p>
Bestellt am (*)/erhalten am (*): ______________________<br />
Name des/der Verbraucher(s): ______________________<br />
Anschrift des/der Verbraucher(s): ______________________<br />
Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier): ______________________<br />
Datum: ______________________
</p>
<p class="small">(*) Unzutreffendes streichen.</p>
</div>
</div>
</section>
</Layout>
+121
View File
@@ -0,0 +1,121 @@
---
import Layout from "../../layouts/Layout.astro";
import ProductCard from "../../components/ProductCard.astro";
import { products, getProduct, productsByCategory } from "../../data/products";
import { getCategory } from "../../data/categories";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
import { formatPrice } from "../../i18n/format";
export function getStaticPaths() {
return products.map((p) => ({ params: { slug: p.slug } }));
}
const lang: Locale = "de";
const t = useTranslations(lang);
const { slug } = Astro.params;
const product = getProduct(slug!)!;
const category = getCategory(product.kategorie);
const empfehlungen = productsByCategory(product.kategorie).filter((p) => p.slug !== product.slug).slice(0, 3);
const name = product.name[lang];
const desc = product.beschreibung[lang];
const badgeLabel: Record<string, string> = { neu: t.badge.neu, bestseller: t.badge.bestseller, sale: t.badge.sale, handgemacht: t.badge.handgemacht };
const felder: [string, string | undefined][] = [
[t.product.articleNo, product.artikelnummer],
[t.product.material, product.material],
[t.product.size, product.groesse],
[t.product.colors, product.farben?.join(", ")],
[t.product.included, product.lieferumfang],
[t.product.care, product.pflegehinweise],
[t.product.delivery, product.lieferzeit],
];
---
<Layout title={name} description={desc} lang={lang} path={`/produkt/${product.slug}/`}>
<section class="section-tight">
<div class="container">
<p class="small breadcrumb">
<a href="/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/shop/${product.kategorie}/`}>{category?.name[lang]}</a> / {name}
</p>
<div class="grid grid-2 product-detail">
<div>
<div class="img-placeholder product-hero" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
{t.common.photoSoon}<br /><span class="small">{t.product.photoNote}</span>
</div>
<div class="thumb-row">
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
<div class="img-placeholder thumb"></div>
</div>
</div>
<div>
<div class="badge-row">
{product.badges.map((b) => <span class={`badge ${b === "sale" ? "badge-sale" : ""}`}>{badgeLabel[b]}</span>)}
{product.bestand === 0 && <span class="badge badge-sold-out">{t.badge.ausverkauft}</span>}
</div>
<h1>{name}</h1>
<div class="price-row-detail">
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
<span class="price-detail">{formatPrice(product.preis, lang)}</span>
</div>
<p class="small">{t.common.vatNote} · {t.common.plusShipping}</p>
<p class="lead">{desc}</p>
<div class="qty-row">
<label for="qty">{t.common.quantity}</label>
<input type="number" id="qty" min="1" value="1" />
<button
class="btn btn-primary"
id="add-to-cart"
disabled={product.bestand === 0}
data-slug={product.slug}
data-name={name}
data-preis={product.preis}
>
{product.bestand === 0 ? t.common.soldOut : t.common.addToCart}
</button>
</div>
<p class="small" id="add-confirm" role="status" style="display:none; color: var(--c-accent);">{t.common.addedToCart}</p>
{felder.some(([, v]) => v) && (
<table class="specs">
<tbody>
{felder.filter(([, v]) => v).map(([k, v]) => (
<tr><th>{k}</th><td>{v}</td></tr>
))}
</tbody>
</table>
)}
<div class="todo-note">{t.product.todoFields}</div>
</div>
</div>
</div>
</section>
{empfehlungen.length > 0 && (
<section class="section-tight">
<div class="container">
<h2>{t.product.recommendations}</h2>
<div class="grid grid-3">{empfehlungen.map((p) => <ProductCard product={p} lang={lang} />)}</div>
</div>
</section>
)}
</Layout>
<script>
import { addToCart } from "../../scripts/cart";
const btn = document.getElementById("add-to-cart") as HTMLButtonElement | null;
const qtyInput = document.getElementById("qty") as HTMLInputElement;
const confirm = document.getElementById("add-confirm")!;
btn?.addEventListener("click", () => {
const { slug, name, preis } = btn.dataset;
addToCart({ slug: slug!, name: name!, preis: Number(preis) }, Math.max(1, Number(qtyInput.value) || 1));
confirm.style.display = "block";
setTimeout(() => (confirm.style.display = "none"), 2500);
});
</script>
+31
View File
@@ -0,0 +1,31 @@
---
import Layout from "../../layouts/Layout.astro";
import ProductCard from "../../components/ProductCard.astro";
import { saleProducts } from "../../data/products";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
const items = saleProducts();
---
<Layout title="Sale" description="Aktuelle Angebote bei Van's DIY & Bastelbedarf." lang={lang} path="/sale/">
<div class="container">
<section class="page-banner" style="background-image:url('/img/banner-sale.webp'); --banner-ratio: 1600 / 639;">
<div class="page-banner-content">
<span class="eyebrow">{t.sale.eyebrow}</span>
<h1>{t.sale.title}</h1>
<p class="lead">{t.sale.lead}</p>
</div>
</section>
</div>
<section class="section-tight">
<div class="container">
{items.length > 0 ? (
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
) : (
<p class="small">{t.sale.empty}</p>
)}
</div>
</section>
</Layout>
+49
View File
@@ -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>
+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 = "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>
+83
View File
@@ -0,0 +1,83 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Über die Kreative" description="Lerne VanVan kennen die Person hinter Van's DIY & Bastelbedarf." lang={lang} path="/ueber-die-kreative/">
<section class="section-tight">
<div class="container grid grid-2 intro">
<div class="portrait-frame portrait">
<img src="/img/vanvan-portrait-holding.webp" alt="VanVan in ihrer Werkstatt mit selbstgehäkelten Plushies" width="900" height="1353" loading="lazy" />
<span class="portrait-badge">{t.common.handmadeBadge}</span>
</div>
<div>
<span class="eyebrow">{t.about.eyebrow}</span>
<h1>{t.about.title}</h1>
<p class="lead">{t.about.lead}</p>
</div>
</div>
</section>
<section class="section-tight">
<div class="container story">
{t.about.story.map((s) => (
<div class="story-section">
<h2>{s.title}</h2>
{s.paragraphs.map((p) => <p>{p}</p>)}
</div>
))}
</div>
</section>
<section class="section-tight">
<div class="container">
<h2 class="text-center">{t.about.valuesTitle}</h2>
<div class="grid grid-3">
{t.about.values.map((v) => (
<div class="card text-center">
<h3>{v.icon} {v.title}</h3>
<p class="small">{v.text}</p>
</div>
))}
</div>
</div>
</section>
<section class="section-tight">
<div class="container">
<blockquote class="pull-quote pull-quote-center">„{t.about.closing}”</blockquote>
</div>
</section>
<section class="section-tight">
<div class="container text-center">
<a class="btn btn-primary" href="/shop/">{t.common.toShop}</a>
</div>
</section>
</Layout>
<script>
// Dezente Scroll-Animation für die Story-Abschnitte (Wunsch aus Dokument1.pdf: "sanfte
// Scroll-Animationen sind willkommen"). Fällt sauber zurück, falls IntersectionObserver
// fehlt oder der Nutzer reduzierte Bewegung bevorzugt (siehe CSS prefers-reduced-motion).
const sections = document.querySelectorAll(".story-section");
if ("IntersectionObserver" in window && sections.length) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.15 }
);
sections.forEach((el) => io.observe(el));
} else {
sections.forEach((el) => el.classList.add("is-visible"));
}
</script>
+35
View File
@@ -0,0 +1,35 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Versand & Zahlung" description="Versandbedingungen und Zahlungsarten bei Van's DIY & Bastelbedarf." lang={lang} path="/versand-zahlung/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.shipping.eyebrow}</span>
<h1>{t.shipping.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container grid grid-2">
<div class="card">
<h3>{t.shipping.shippingTitle}</h3>
<ul>
{t.shipping.shippingItems.map((item) => <li>{item}</li>)}
</ul>
</div>
<div class="card">
<h3>{t.shipping.paymentTitle}</h3>
<ul>
{t.shipping.paymentItems.map((item) => <li>{item}</li>)}
</ul>
<p class="small">{t.common.vatNote}</p>
</div>
</div>
<p class="todo-note">{t.shipping.todoNote}</p>
</section>
</Layout>
+101
View File
@@ -0,0 +1,101 @@
---
import Layout from "../../layouts/Layout.astro";
import type { Locale } from "../../i18n/config";
import { useTranslations } from "../../i18n/ui";
const lang: Locale = "de";
const t = useTranslations(lang);
---
<Layout title="Warenkorb" description="Dein Warenkorb bei Van's DIY & Bastelbedarf." lang={lang} path="/warenkorb/">
<section class="section-tight">
<div class="container">
<span class="eyebrow">{t.cart.eyebrow}</span>
<h1>{t.cart.title}</h1>
</div>
</section>
<section class="section-tight">
<div class="container">
<div id="cart-empty" class="card" style="display:none;">
<p>{t.cart.empty}</p>
<a class="btn btn-primary" href="/shop/">{t.common.browseShop}</a>
</div>
<div id="cart-content" class="cart-layout" style="display:none;">
<div class="card cart-items" id="cart-items"></div>
<aside class="card cart-summary">
<h3>{t.cart.subtotal}</h3>
<div class="summary-row"><span>{t.cart.subtotal}</span><span id="sum-subtotal">0,00 €</span></div>
<div class="summary-row"><span>{t.cart.shipping}</span><span id="sum-shipping">{t.cart.shippingCalculated}</span></div>
<p class="small" id="shipping-hint"></p>
<hr class="divider" />
<div class="summary-row total"><span>{t.cart.total}</span><span id="sum-total">0,00 €</span></div>
<p class="small">{t.common.vatNote}</p>
<a class="btn btn-primary btn-block" href="/checkout/">{t.cart.toCheckout}</a>
</aside>
</div>
</div>
</section>
</Layout>
<script define:vars={{ freeShipFrom: 75, removeLabel: t.cart.remove, perItemLabel: t.cart.perItem, remainingTemplate: t.cart.remaining("__V__"), freeShippingText: t.cart.freeShipping, cartLang: lang }}>
import { getCart, updateQuantity, removeFromCart, cartTotal } from "../../scripts/cart";
import { formatPrice } from "../../i18n/format";
function render() {
const cart = getCart();
const empty = document.getElementById("cart-empty");
const content = document.getElementById("cart-content");
const itemsEl = document.getElementById("cart-items");
if (cart.length === 0) {
empty.style.display = "block";
content.style.display = "none";
return;
}
empty.style.display = "none";
content.style.display = "grid";
itemsEl.innerHTML = cart.map((i) => `
<div class="cart-item">
<div class="img-placeholder thumb"></div>
<div class="info">
<strong>${i.name}</strong>
<div class="small">${formatPrice(i.preis, cartLang)} ${perItemLabel}</div>
</div>
<div class="qty-controls">
<button data-action="dec" data-slug="${i.slug}"></button>
<span>${i.menge}</span>
<button data-action="inc" data-slug="${i.slug}">+</button>
</div>
<div><strong>${formatPrice(i.preis * i.menge, cartLang)}</strong></div>
<a href="#" class="remove" data-action="remove" data-slug="${i.slug}">${removeLabel}</a>
</div>
`).join("");
const subtotal = cartTotal();
const remaining = Math.max(0, freeShipFrom - subtotal);
document.getElementById("sum-subtotal").textContent = formatPrice(subtotal, cartLang);
document.getElementById("sum-total").textContent = formatPrice(subtotal, cartLang);
document.getElementById("shipping-hint").textContent =
remaining > 0 ? remainingTemplate.replace("__V__", formatPrice(remaining, cartLang)) : freeShippingText;
itemsEl.querySelectorAll("button, a.remove").forEach((el) => {
el.addEventListener("click", (e) => {
e.preventDefault();
const target = e.currentTarget;
const slug = target.dataset.slug;
const action = target.dataset.action;
const item = getCart().find((i) => i.slug === slug);
if (!item) return;
if (action === "inc") updateQuantity(slug, item.menge + 1);
if (action === "dec") updateQuantity(slug, item.menge - 1);
if (action === "remove") removeFromCart(slug);
render();
});
});
}
render();
window.addEventListener("cart:changed", render);
</script>
+71
View File
@@ -0,0 +1,71 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout title="Widerrufsbelehrung" path="/widerruf/" legalOnly={true}>
<section class="section-tight">
<div class="container">
<h1>Widerrufsbelehrung</h1>
<div class="legal-note">
⚠️ <strong>Muster-Text.</strong> Vorbereiteter Platzhalter, ersetzt keine rechtliche
Prüfung durch eine fachkundige Stelle vor Veröffentlichung.
</div>
<h2>Widerrufsrecht</h2>
<p>
Du hast das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu
widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem du oder ein von dir
benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen hast bzw. hat.
</p>
<p>
Um dein Widerrufsrecht auszuüben, musst du uns ([Vorname Nachname], Van's DIY &
Bastelbedarf, [Anschrift], [E-Mail-Adresse]) mittels einer eindeutigen Erklärung (z. B. ein
mit der Post versandter Brief oder E-Mail) über deinen Entschluss, diesen Vertrag zu
widerrufen, informieren. Du kannst dafür das beigefügte
<a href="/muster-widerrufsformular/">Muster-Widerrufsformular</a> verwenden, das jedoch
nicht vorgeschrieben ist.
</p>
<p>
Zur Wahrung der Widerrufsfrist reicht es aus, dass du die Mitteilung über die Ausübung des
Widerrufsrechts vor Ablauf der Widerrufsfrist absendest.
</p>
<h2>Folgen des Widerrufs</h2>
<p>
Wenn du diesen Vertrag widerrufst, haben wir dir alle Zahlungen, die wir von dir erhalten
haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich
daraus ergeben, dass du eine andere Art der Lieferung als die von uns angebotene, günstigste
Standardlieferung gewählt hast), unverzüglich und spätestens binnen vierzehn Tagen ab dem
Tag zurückzuzahlen, an dem die Mitteilung über deinen Widerruf dieses Vertrags bei uns
eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das du bei der
ursprünglichen Transaktion eingesetzt hast, es sei denn, mit dir wurde ausdrücklich etwas
anderes vereinbart; in keinem Fall werden dir wegen dieser Rückzahlung Entgelte berechnet.
</p>
<p>
Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder
bis du den Nachweis erbracht hast, dass du die Waren zurückgesandt hast, je nachdem, welches
der frühere Zeitpunkt ist.
</p>
<p>
Du hast die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem
Tag, an dem du uns über den Widerruf dieses Vertrags unterrichtest, an uns zurückzusenden
oder zu übergeben. Die Frist ist gewahrt, wenn du die Waren vor Ablauf der Frist von
vierzehn Tagen absendest. Du trägst die unmittelbaren Kosten der Rücksendung der Waren.
</p>
<p>
Du musst für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf
einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht
notwendigen Umgang mit ihnen zurückzuführen ist.
</p>
<h2>Ausschluss/vorzeitiges Erlöschen des Widerrufsrechts</h2>
<p>
Das Widerrufsrecht besteht nicht bzw. erlischt vorzeitig unter anderem bei Verträgen zur
Lieferung von Waren, die nicht vorgefertigt sind und für deren Herstellung eine individuelle
Auswahl oder Bestimmung durch dich maßgeblich ist oder die eindeutig auf deine persönlichen
Bedürfnisse zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB) — z. B. bei individuellen
Sonderanfertigungen auf Kundenwunsch. Bei Standardartikeln aus unserem Sortiment gilt das
Widerrufsrecht regulär.
</p>
</div>
</section>
</Layout>