Initial commit: Van's DIY & Bastelbedarf Astro shop
This commit is contained in:
@@ -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 2–7 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 l’Exemple" 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import { categories, getCategory } from "../../data/categories";
|
||||
import { productsByCategory } from "../../data/products";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
|
||||
export function getStaticPaths() {
|
||||
return categories.map((c) => ({ params: { kategorie: c.slug } }));
|
||||
}
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
const { kategorie } = Astro.params;
|
||||
const category = getCategory(kategorie!)!;
|
||||
const items = productsByCategory(category.slug);
|
||||
---
|
||||
<Layout title={category.name[lang]} description={category.description[lang]} lang={lang} path={`/shop/${category.slug}/`}>
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small breadcrumb"><a href="/shop/">{t.shop.breadcrumbShop}</a> / {category.name[lang]}</p>
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon}</span>
|
||||
<h1>{category.name[lang]}</h1>
|
||||
<p class="lead">{category.description[lang]}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
{category.subcategories && category.subcategories.length > 0 && (
|
||||
<>
|
||||
<h2 class="text-center small" style="text-transform:uppercase; letter-spacing:0.08em; color:var(--c-text-muted); margin-bottom:0.8rem;">{t.shop.subcategoriesLabel}</h2>
|
||||
<div class="subcategory-chips">
|
||||
{category.subcategories.map((s) => (
|
||||
<a class="subcategory-chip" href={`/shop/${category.slug}/${s.slug}/`}>{s.name[lang]}</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{items.length > 0 ? (
|
||||
<div class="grid grid-3">{items.map((p) => <ProductCard product={p} lang={lang} />)}</div>
|
||||
) : (
|
||||
<p class="small">{t.shop.categoryEmpty}</p>
|
||||
)}
|
||||
<p class="legal-hint small">{t.common.vatNote} {t.common.allPricesPlusShipping}</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import Layout from "../../../layouts/Layout.astro";
|
||||
import { categories, getCategory, getSubcategory } from "../../../data/categories";
|
||||
import type { Locale } from "../../../i18n/config";
|
||||
import { useTranslations } from "../../../i18n/ui";
|
||||
|
||||
export function getStaticPaths() {
|
||||
return categories.flatMap((c) =>
|
||||
(c.subcategories ?? []).map((s) => ({ params: { kategorie: c.slug, unterkategorie: s.slug } }))
|
||||
);
|
||||
}
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
const { kategorie, unterkategorie } = Astro.params;
|
||||
const category = getCategory(kategorie!)!;
|
||||
const sub = getSubcategory(kategorie!, unterkategorie!)!;
|
||||
---
|
||||
<Layout title={`${sub.name[lang]} – ${category.name[lang]}`} description={`${sub.name[lang]} bei Van's DIY & Bastelbedarf.`} lang={lang} path={`/shop/${category.slug}/${sub.slug}/`}>
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small breadcrumb">
|
||||
<a href="/shop/">{t.shop.breadcrumbShop}</a> / <a href={`/shop/${category.slug}/`}>{category.name[lang]}</a> / {sub.name[lang]}
|
||||
</p>
|
||||
<span class="eyebrow" aria-hidden="true">{category.icon}</span>
|
||||
<h1>{sub.name[lang]}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container">
|
||||
<p class="small">{t.shop.subcategoryEmpty}</p>
|
||||
<div class="btn-row">
|
||||
<a class="btn btn-outline" href={`/shop/${category.slug}/`}>← {category.name[lang]}</a>
|
||||
<a class="btn btn-primary" href="/shop/">{t.common.toShop}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import ProductCard from "../../components/ProductCard.astro";
|
||||
import { categories } from "../../data/categories";
|
||||
import { products } from "../../data/products";
|
||||
import type { Locale } from "../../i18n/config";
|
||||
import { useTranslations } from "../../i18n/ui";
|
||||
|
||||
const lang: Locale = "de";
|
||||
const t = useTranslations(lang);
|
||||
---
|
||||
<Layout title="Shop" description="Der komplette Shop von Van's DIY & Bastelbedarf – Häkelwerke, Plushies, Schmuck und Bastelzubehör." lang={lang} path="/shop/">
|
||||
<div class="container">
|
||||
<section class="page-banner" style="background-image:url('/img/banner-shop.webp'); --banner-ratio: 1600 / 729;">
|
||||
<div class="page-banner-content">
|
||||
<span class="eyebrow">{t.shop.breadcrumbShop}</span>
|
||||
<h1>{t.shop.title}</h1>
|
||||
<p class="lead">{t.shop.lead}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="section-tight">
|
||||
<div class="container shop-layout">
|
||||
<aside class="filters card" aria-label={t.shop.filters}>
|
||||
<h3>{t.shop.filters}</h3>
|
||||
<div class="filter-group">
|
||||
<label for="f-kategorie">{t.shop.categoryLabel}</label>
|
||||
<select id="f-kategorie">
|
||||
<option value="">{t.shop.allCategories}</option>
|
||||
{categories.map((c) => <option value={c.slug}>{c.name[lang]}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-preis">{t.shop.priceUpTo}</label>
|
||||
<input id="f-preis" type="range" min="0" max="110" value="110" />
|
||||
<span class="small" id="f-preis-value">{t.shop.priceUpToValue(110)}</span>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="f-verfuegbar">{t.shop.onlyAvailable}</label>
|
||||
<input id="f-verfuegbar" type="checkbox" />
|
||||
</div>
|
||||
<p class="small">{t.shop.filterNote}</p>
|
||||
</aside>
|
||||
|
||||
<div>
|
||||
<div class="shop-toolbar">
|
||||
<span class="small" id="result-count">{t.shop.resultCount(products.length)}</span>
|
||||
<select id="sort" aria-label={t.shop.sortLabel}>
|
||||
<option value="neu">{t.shop.sortNew}</option>
|
||||
<option value="bestseller">{t.shop.sortBestseller}</option>
|
||||
<option value="preis-auf">{t.shop.sortPriceAsc}</option>
|
||||
<option value="preis-ab">{t.shop.sortPriceDesc}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid grid-3" id="product-grid">
|
||||
{products.map((p) => (
|
||||
<div
|
||||
class="product-slot"
|
||||
data-kategorie={p.kategorie}
|
||||
data-preis={p.preis}
|
||||
data-bestand={p.bestand}
|
||||
data-neu={p.badges.includes("neu") ? 1 : 0}
|
||||
data-bestseller={p.badges.includes("bestseller") ? 1 : 0}
|
||||
>
|
||||
<ProductCard product={p} lang={lang} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p class="small" id="empty-msg" style="display:none;">{t.shop.noResults}</p>
|
||||
<p class="legal-hint small">{t.common.vatNote} {t.common.allPricesPlusShipping}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ productSingular: t.shop.productSingular, productPlural: t.shop.productPlural, unitFirst: lang === "en" }}>
|
||||
const grid = document.getElementById("product-grid");
|
||||
const slots = Array.from(grid.querySelectorAll(".product-slot"));
|
||||
const kategorieSel = document.getElementById("f-kategorie");
|
||||
const preisRange = document.getElementById("f-preis");
|
||||
const preisValue = document.getElementById("f-preis-value");
|
||||
const verfuegbarChk = document.getElementById("f-verfuegbar");
|
||||
const sortSel = document.getElementById("sort");
|
||||
const resultCount = document.getElementById("result-count");
|
||||
const emptyMsg = document.getElementById("empty-msg");
|
||||
const currencySymbol = "€";
|
||||
|
||||
function formatPrice(v) {
|
||||
return unitFirst ? `${currencySymbol}${v}` : `${v} ${currencySymbol}`;
|
||||
}
|
||||
|
||||
function apply() {
|
||||
const kat = kategorieSel.value;
|
||||
const maxPreis = Number(preisRange.value);
|
||||
const nurVerfuegbar = verfuegbarChk.checked;
|
||||
preisValue.textContent = formatPrice(maxPreis);
|
||||
|
||||
let visible = 0;
|
||||
slots.forEach((slot) => {
|
||||
const matchKat = !kat || slot.dataset.kategorie === kat;
|
||||
const matchPreis = Number(slot.dataset.preis) <= maxPreis;
|
||||
const matchVerfuegbar = !nurVerfuegbar || Number(slot.dataset.bestand) > 0;
|
||||
const show = matchKat && matchPreis && matchVerfuegbar;
|
||||
slot.style.display = show ? "" : "none";
|
||||
if (show) visible++;
|
||||
});
|
||||
|
||||
resultCount.textContent = `${visible} ${visible === 1 ? productSingular : productPlural}`;
|
||||
emptyMsg.style.display = visible === 0 ? "block" : "none";
|
||||
|
||||
const sorted = [...slots].sort((a, b) => {
|
||||
switch (sortSel.value) {
|
||||
case "preis-auf": return Number(a.dataset.preis) - Number(b.dataset.preis);
|
||||
case "preis-ab": return Number(b.dataset.preis) - Number(a.dataset.preis);
|
||||
case "bestseller": return Number(b.dataset.bestseller) - Number(a.dataset.bestseller);
|
||||
default: return Number(b.dataset.neu) - Number(a.dataset.neu);
|
||||
}
|
||||
});
|
||||
sorted.forEach((el) => grid.appendChild(el));
|
||||
}
|
||||
|
||||
[kategorieSel, preisRange, verfuegbarChk, sortSel].forEach((el) => el.addEventListener("input", apply));
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const preset = params.get("kategorie");
|
||||
if (preset) kategorieSel.value = preset;
|
||||
|
||||
apply();
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user