Initial commit: Van's DIY & Bastelbedarf Astro shop
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import { locales, localeMeta, type Locale } from "../i18n/config";
|
||||
|
||||
interface Props {
|
||||
lang: Locale;
|
||||
/** Sprachneutraler Pfad der aktuellen Seite, z.B. "/shop/" oder "/produkt/foo/". */
|
||||
path: string;
|
||||
/** Seite existiert nur auf Deutsch (z.B. Rechtstexte) — alle Optionen führen zur DE-Version,
|
||||
* damit der Umschalter niemals auf eine 404-Seite verlinkt. */
|
||||
legalOnly?: boolean;
|
||||
}
|
||||
const { lang, path, legalOnly = false } = Astro.props;
|
||||
|
||||
function hrefFor(target: Locale) {
|
||||
if (legalOnly) return path; // immer die deutsche Original-URL
|
||||
const prefix = target === "de" ? "" : `/${target}`;
|
||||
return `${prefix}${path}` || "/";
|
||||
}
|
||||
|
||||
function globeStyle(l: Locale) {
|
||||
const m = localeMeta[l];
|
||||
return `--globe-color:${m.color}; --globe-glow:${m.glow}; --flag-1:${m.flag[0]}; --flag-2:${m.flag[1]}; --flag-3:${m.flag[2]}`;
|
||||
}
|
||||
---
|
||||
<div class="lang-switch" id="lang-switch">
|
||||
<button
|
||||
type="button"
|
||||
class="lang-switch-btn"
|
||||
id="lang-switch-btn"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
style={globeStyle(lang)}
|
||||
>
|
||||
<span class="lang-globe" aria-hidden="true">
|
||||
<span class="lang-globe-rotor">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="8.5" />
|
||||
<ellipse cx="12" cy="12" rx="3.6" ry="8.5" />
|
||||
<path d="M3.7 9.5h16.6M3.7 14.5h16.6" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span class="lang-label">{localeMeta[lang].label}</span>
|
||||
<span class="chevron">▼</span>
|
||||
</button>
|
||||
<div class="lang-switch-menu" role="menu">
|
||||
{locales.map((l) => (
|
||||
<a
|
||||
href={hrefFor(l)}
|
||||
role="menuitem"
|
||||
aria-current={l === lang ? "true" : "false"}
|
||||
title={legalOnly && l !== "de" ? "Nur auf Deutsch verfügbar / Only available in German" : undefined}
|
||||
style={globeStyle(l)}
|
||||
>
|
||||
<span class="lang-globe" aria-hidden="true">
|
||||
<span class="lang-globe-rotor">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">
|
||||
<circle cx="12" cy="12" r="8.5" />
|
||||
<ellipse cx="12" cy="12" rx="3.6" ry="8.5" />
|
||||
<path d="M3.7 9.5h16.6M3.7 14.5h16.6" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span class="lang-label">{localeMeta[l].label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Jede Instanz für sich initialisieren (Desktop-Header + evtl. Mobile-Nav-Kopie).
|
||||
document.querySelectorAll(".lang-switch").forEach((el) => {
|
||||
const btn = el.querySelector(".lang-switch-btn");
|
||||
btn?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll(".lang-switch.open").forEach((other) => {
|
||||
if (other !== el) other.classList.remove("open");
|
||||
});
|
||||
el.classList.toggle("open");
|
||||
btn.setAttribute("aria-expanded", el.classList.contains("open") ? "true" : "false");
|
||||
});
|
||||
});
|
||||
document.addEventListener("click", () => {
|
||||
document.querySelectorAll(".lang-switch.open").forEach((el) => el.classList.remove("open"));
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
document.querySelectorAll(".lang-switch.open").forEach((el) => el.classList.remove("open"));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
import type { Product } from "../data/products";
|
||||
import type { Locale } from "../i18n/config";
|
||||
import { localePrefix } from "../i18n/config";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
import { formatPrice } from "../i18n/format";
|
||||
|
||||
interface Props { product: Product; lang: Locale }
|
||||
const { product, lang } = Astro.props;
|
||||
const t = useTranslations(lang);
|
||||
|
||||
const badgeLabel: Record<string, string> = {
|
||||
neu: t.badge.neu,
|
||||
bestseller: t.badge.bestseller,
|
||||
sale: t.badge.sale,
|
||||
handgemacht: t.badge.handgemacht,
|
||||
};
|
||||
const name = product.name[lang];
|
||||
const desc = product.beschreibung[lang];
|
||||
---
|
||||
<a href={`${localePrefix(lang)}/produkt/${product.slug}/`} class="card product-card">
|
||||
<div class="product-media">
|
||||
<div class="img-placeholder" role="img" aria-label={`${t.common.photoSoon}: ${name}`}>
|
||||
{t.common.photoSoon}<br /><span class="small">{name}</span>
|
||||
</div>
|
||||
<div class="product-badges">
|
||||
{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>
|
||||
</div>
|
||||
<h3>{name}</h3>
|
||||
<p class="small">{desc.slice(0, 78)}{desc.length > 78 ? "…" : ""}</p>
|
||||
<div class="price-row">
|
||||
{product.preisAlt && <span class="price-old">{formatPrice(product.preisAlt, lang)}</span>}
|
||||
<span class="price">{formatPrice(product.preis, lang)}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.product-card { display: flex; flex-direction: column; gap: 0.6rem; padding: 1rem; }
|
||||
.product-media { position: relative; }
|
||||
.product-media .img-placeholder { aspect-ratio: 1 / 1; min-height: 0; }
|
||||
.product-badges { position: absolute; top: 0.6rem; left: 0.6rem; display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
.product-card h3 { font-size: 1.05rem; margin: 0.2rem 0 0; transition: color 0.2s var(--ease); }
|
||||
.product-card:hover h3 { color: var(--c-accent); }
|
||||
.product-card p { margin: 0; }
|
||||
.price-row { margin-top: auto; display: flex; align-items: baseline; gap: 0.6rem; }
|
||||
.price { color: var(--c-accent); font-weight: 700; font-size: 1.15rem; }
|
||||
.price-old { color: var(--c-text-muted); text-decoration: line-through; font-size: 0.9rem; }
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { Locale } from "../i18n/config";
|
||||
|
||||
// Hauptkategorien laut Pflichtenheft (Abschnitt 6 "Shop & Kategorien").
|
||||
// Neue Kategorien/Unterkategorien einfach hier ergänzen — alle Shop-Routen lesen aus dieser Datei,
|
||||
// nichts ist fest verdrahtet ("beliebig viele Unterkategorien ... müssen möglich sein").
|
||||
// name/description sind je Sprache hinterlegt (DE/EN/CH/FR) für die Mehrsprachigkeit.
|
||||
//
|
||||
// Unterkategorien (seit 02.08.2026, auf VanVans Wunsch): bereits vollständig angelegt, auch
|
||||
// ohne dass schon Produkte zugeordnet sind — "damit später Produkte direkt den entsprechenden
|
||||
// Bereichen zugeordnet werden können" (siehe Dokument1.pdf). Zeigen bis dahin einen
|
||||
// "noch keine Produkte"-Hinweis auf ihrer eigenen Seite.
|
||||
|
||||
type LocalizedText = Record<Locale, string>;
|
||||
|
||||
export interface Subcategory {
|
||||
slug: string;
|
||||
name: LocalizedText;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
slug: string;
|
||||
name: LocalizedText;
|
||||
description: LocalizedText;
|
||||
icon: string; // dezentes Emoji als Platzhalter-Icon, bis es echte Icons/Fotos gibt
|
||||
subcategories?: Subcategory[];
|
||||
}
|
||||
|
||||
export const categories: Category[] = [
|
||||
{
|
||||
slug: "diy-plushies",
|
||||
name: { de: "DIY Plushies", en: "DIY Plushies", ch: "DIY Plushies", fr: "Peluches DIY" },
|
||||
description: {
|
||||
de: "Handgenähte Kuscheltiere und Plüschfiguren – jedes Stück ein Unikat.",
|
||||
en: "Hand-sewn stuffed animals and plush figures – every piece a one-of-a-kind.",
|
||||
ch: "Vo Hang gnähti Kuscheltier und Plüschfigure – jedes Sächeli es Unikat.",
|
||||
fr: "Peluches et figurines cousues à la main – chaque pièce est unique.",
|
||||
},
|
||||
icon: "🧸",
|
||||
subcategories: [
|
||||
{ slug: "kuh-frieda", name: { de: "Kuh „Frieda”", en: "Cow \"Frieda\"", ch: "Kuh „Frieda”", fr: "Vache « Frieda »" } },
|
||||
{ slug: "hase-hasidog", name: { de: "Hase „HasiDog”", en: "Bunny \"HasiDog\"", ch: "Hase „HasiDog”", fr: "Lapin « HasiDog »" } },
|
||||
{ slug: "wal-sam", name: { de: "Wal „Sam”", en: "Whale \"Sam\"", ch: "Wal „Sam”", fr: "Baleine « Sam »" } },
|
||||
{ slug: "oktopus-siggi", name: { de: "Oktopus „Siggi”", en: "Octopus \"Siggi\"", ch: "Oktopus „Siggi”", fr: "Poulpe « Siggi »" } },
|
||||
{ slug: "schildkroete-gaby", name: { de: "Schildkröte „Gaby”", en: "Turtle \"Gaby\"", ch: "Schildchröte „Gaby”", fr: "Tortue « Gaby »" } },
|
||||
{ slug: "pinguin-pingi", name: { de: "Pinguin „Pingi”", en: "Penguin \"Pingi\"", ch: "Pinguin „Pingi”", fr: "Pingouin « Pingi »" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "diy-haekelwerke",
|
||||
name: { de: "DIY Häkelwerke", en: "DIY Crochet", ch: "DIY Häkelwerke", fr: "Crochet DIY" },
|
||||
description: {
|
||||
de: "Gehäkelte Deko, Accessoires und kleine Kunstwerke aus feiner Wolle.",
|
||||
en: "Crocheted décor, accessories and small works of art made from fine yarn.",
|
||||
ch: "Ghäkleti Deko, Accessoires und chlini Kunstwerch us feiner Wolle.",
|
||||
fr: "Décorations, accessoires et petites œuvres crochetés en laine fine.",
|
||||
},
|
||||
icon: "🧶",
|
||||
},
|
||||
{
|
||||
slug: "modeschmuck-accessoires",
|
||||
name: { de: "DIY Modeschmuck & Accessoires", en: "DIY Jewelry & Accessories", ch: "DIY Modeschmuck & Accessoires", fr: "Bijoux & accessoires DIY" },
|
||||
description: {
|
||||
de: "Selbstgemachter Schmuck und Accessoires für den besonderen Auftritt.",
|
||||
en: "Handmade jewelry and accessories for that special touch.",
|
||||
ch: "Sälber gmachte Schmuck und Accessoires für dr bsundere Uftritt.",
|
||||
fr: "Bijoux et accessoires faits main pour une touche unique.",
|
||||
},
|
||||
icon: "📿",
|
||||
subcategories: [
|
||||
{ slug: "glasperlenarmbaender", name: { de: "Glasperlenarmbänder", en: "Glass Bead Bracelets", ch: "Glasperle-Armbändel", fr: "Bracelets en perles de verre" } },
|
||||
{ slug: "kunststoffperlenarmbaender", name: { de: "Kunststoffperlenarmbänder", en: "Plastic Bead Bracelets", ch: "Kunststoffperle-Armbändel", fr: "Bracelets en perles plastique" } },
|
||||
{ slug: "edelstahlhalsketten", name: { de: "Edelstahlhalsketten", en: "Stainless Steel Necklaces", ch: "Edelstahl-Halschette", fr: "Colliers en acier inoxydable" } },
|
||||
{ slug: "polyesterhalsketten", name: { de: "Polyesterhalsketten", en: "Polyester Necklaces", ch: "Polyester-Halschette", fr: "Colliers en polyester" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "haekelzubehoer",
|
||||
name: { de: "Häkelzubehör", en: "Crochet Supplies", ch: "Häkelzubehör", fr: "Fournitures de crochet" },
|
||||
description: {
|
||||
de: "Nadeln, Garne und alles, was dein nächstes Häkelprojekt braucht.",
|
||||
en: "Hooks, yarn and everything your next crochet project needs.",
|
||||
ch: "Nadle, Garn und alles, wo's für dis nächschte Häkelprojekt bruucht.",
|
||||
fr: "Crochets, fils et tout ce qu'il faut pour votre prochain projet.",
|
||||
},
|
||||
icon: "🪡",
|
||||
subcategories: [
|
||||
{ slug: "maschenmarkierer", name: { de: "Maschenmarkierer", en: "Stitch Markers", ch: "Masche-Markierer", fr: "Marqueurs de mailles" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "bastelzubehoer",
|
||||
name: { de: "Bastelzubehör", en: "Craft Supplies", ch: "Bastelzubehör", fr: "Fournitures créatives" },
|
||||
description: {
|
||||
de: "Grundausstattung und kleine Helfer für kreative DIY-Projekte.",
|
||||
en: "Essentials and handy little helpers for creative DIY projects.",
|
||||
ch: "Grundusstattig und chlini Helfer für kreativi DIY-Projäkt.",
|
||||
fr: "L'essentiel et de petits accessoires pour vos projets créatifs.",
|
||||
},
|
||||
icon: "✂️",
|
||||
subcategories: [
|
||||
{ slug: "holzknoepfe", name: { de: "Holzknöpfe", en: "Wooden Buttons", ch: "Holzchnöpf", fr: "Boutons en bois" } },
|
||||
{ slug: "kunststoffknoepfe", name: { de: "Kunststoffknöpfe", en: "Plastic Buttons", ch: "Kunststoffchnöpf", fr: "Boutons en plastique" } },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function getCategory(slug: string): Category | undefined {
|
||||
return categories.find((c) => c.slug === slug);
|
||||
}
|
||||
|
||||
export function getSubcategory(categorySlug: string, subSlug: string): Subcategory | undefined {
|
||||
return getCategory(categorySlug)?.subcategories?.find((s) => s.slug === subSlug);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import type { Locale } from "../i18n/config";
|
||||
|
||||
// Platzhalter-Produktdaten für Phase 1 (rein clientseitige Demo-Daten).
|
||||
// Struktur folgt bewusst den optionalen Feldern aus dem Pflichtenheft (Abschnitt 7
|
||||
// "Produktverwaltung") — nicht gesetzte Felder werden auf den Seiten automatisch
|
||||
// ausgeblendet ("nicht benötigte Felder müssen deaktivierbar sein").
|
||||
//
|
||||
// Mehrsprachigkeit: `name` und `beschreibung` sind je Sprache hinterlegt (DE/EN/CH/FR).
|
||||
// Die technischen Detail-Felder (material, groesse, farben, ...) bleiben bewusst nur auf
|
||||
// Deutsch — das sind ohnehin Platzhalterdaten, die in Phase 2 durch VanVans echte
|
||||
// Produktangaben ersetzt werden. Die zugehörigen LABEL-Texte (z.B. "Material") kommen aus
|
||||
// src/i18n/ui.ts und sind bereits übersetzt.
|
||||
//
|
||||
// TODO Phase 2: diese Datei durch echte Daten aus Cloudflare D1 + Worker-API ersetzen.
|
||||
// TODO VanVan: echte Produktfotos, Texte und Preise liefern — alles hier ist Platzhalter.
|
||||
|
||||
export type Badge = "neu" | "bestseller" | "sale" | "handgemacht";
|
||||
|
||||
type LocalizedText = Record<Locale, string>;
|
||||
|
||||
export interface Product {
|
||||
slug: string;
|
||||
name: LocalizedText;
|
||||
artikelnummer?: string;
|
||||
kategorie: string; // slug aus categories.ts
|
||||
preis: number; // in Euro
|
||||
preisAlt?: number; // falls Sale
|
||||
bestand: number; // 0 = ausverkauft, bleibt trotzdem sichtbar
|
||||
beschreibung: LocalizedText;
|
||||
material?: string;
|
||||
groesse?: string;
|
||||
farben?: string[];
|
||||
lieferumfang?: string;
|
||||
pflegehinweise?: string;
|
||||
lieferzeit?: string;
|
||||
badges: Badge[];
|
||||
}
|
||||
|
||||
export const products: Product[] = [
|
||||
{
|
||||
slug: "wolke-plushie-lavendel",
|
||||
name: {
|
||||
de: "Wolke-Plushie „Lavendel”", en: "Cloud Plushie \"Lavender\"",
|
||||
ch: "Wolke-Plushie „Lavendel”", fr: "Peluche nuage « Lavande »",
|
||||
},
|
||||
artikelnummer: "PLU-001",
|
||||
kategorie: "diy-plushies",
|
||||
preis: 28.5,
|
||||
bestand: 6,
|
||||
beschreibung: {
|
||||
de: "Weiche, handgenähte Wolke zum Kuscheln – aus kuscheligem Plüschstoff mit dezenter Lavendel-Stickerei.",
|
||||
en: "A soft, hand-sewn cloud made for cuddling – plush fabric with subtle lavender embroidery.",
|
||||
ch: "Weichi, vo Hang gnähti Wolke zum Kuschle – us kuschlige Plüschstoff mit dezenter Lavendel-Stickerei.",
|
||||
fr: "Nuage doux cousu à la main, parfait à câliner – tissu peluche avec une broderie lavande discrète.",
|
||||
},
|
||||
material: "Plüschstoff (Polyester), Füllwatte OEKO-TEX",
|
||||
groesse: "ca. 24 × 16 cm",
|
||||
farben: ["Lavendel", "Cremeweiß"],
|
||||
lieferumfang: "1 Plushie",
|
||||
pflegehinweise: "Handwäsche bei 30°C, nicht bügeln.",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["handgemacht", "bestseller"],
|
||||
},
|
||||
{
|
||||
slug: "fuchs-plushie-mini",
|
||||
name: { de: "Mini-Fuchs Plushie", en: "Mini Fox Plushie", ch: "Mini-Fuchs Plushie", fr: "Peluche mini renard" },
|
||||
artikelnummer: "PLU-002",
|
||||
kategorie: "diy-plushies",
|
||||
preis: 19.9,
|
||||
bestand: 0,
|
||||
beschreibung: {
|
||||
de: "Kleiner handgenähter Fuchs für die Tasche oder als Deko im Kinderzimmer.",
|
||||
en: "A small hand-sewn fox to take along or as décor for a kid's room.",
|
||||
ch: "Chline, vo Hang gnähte Fuchs für d'Täsche oder als Deko im Chinderzimmer.",
|
||||
fr: "Petit renard cousu à la main, pour le sac ou pour décorer une chambre d'enfant.",
|
||||
},
|
||||
material: "Baumwollstoff, Füllwatte OEKO-TEX",
|
||||
groesse: "ca. 14 cm",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["handgemacht"],
|
||||
},
|
||||
{
|
||||
slug: "haekeldecke-boho",
|
||||
name: {
|
||||
de: "Häkeldecke „Boho Sonnenuntergang”", en: "Crochet Blanket \"Boho Sunset\"",
|
||||
ch: "Häkeldecke „Boho Sonnenuntergang”", fr: "Plaid au crochet « Coucher de soleil bohème »",
|
||||
},
|
||||
artikelnummer: "HAE-010",
|
||||
kategorie: "diy-haekelwerke",
|
||||
preis: 89,
|
||||
preisAlt: 109,
|
||||
bestand: 3,
|
||||
beschreibung: {
|
||||
de: "Großformatige Häkeldecke in warmen Terracotta- und Lila-Tönen, komplett handgehäkelt.",
|
||||
en: "A large crochet blanket in warm terracotta and purple tones, entirely hand-crocheted.",
|
||||
ch: "Grossformatigi Häkeldecki i warme Terracotta- und Lila-Töne, komplett vo Hang ghäkelt.",
|
||||
fr: "Grand plaid au crochet dans des tons chauds terracotta et violet, entièrement fait main.",
|
||||
},
|
||||
material: "100 % Baumwollgarn",
|
||||
groesse: "ca. 130 × 160 cm",
|
||||
farben: ["Terracotta", "Lila", "Sandbeige"],
|
||||
pflegehinweise: "Handwäsche empfohlen, liegend trocknen.",
|
||||
lieferzeit: "5–7 Werktage (Sonderanfertigung)",
|
||||
badges: ["sale", "handgemacht"],
|
||||
},
|
||||
{
|
||||
slug: "haekel-untersetzer-set",
|
||||
name: { de: "Häkel-Untersetzer Set (4er)", en: "Crochet Coaster Set (4-pack)", ch: "Häkel-Untersetzer Set (4er)", fr: "Set de 4 sous-verres au crochet" },
|
||||
artikelnummer: "HAE-004",
|
||||
kategorie: "diy-haekelwerke",
|
||||
preis: 16,
|
||||
bestand: 14,
|
||||
beschreibung: {
|
||||
de: "Vier runde Untersetzer im Granny-Square-Stil, mixbar in vielen Farben.",
|
||||
en: "Four round coasters in granny-square style, mix and match in many colors.",
|
||||
ch: "Vier rundi Untersetzer im Granny-Square-Stil, mixbar i viel Farbe.",
|
||||
fr: "Quatre sous-verres ronds façon granny square, à combiner dans de nombreuses couleurs.",
|
||||
},
|
||||
material: "Baumwollgarn",
|
||||
lieferumfang: "4 Untersetzer",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["neu"],
|
||||
},
|
||||
{
|
||||
slug: "perlen-armband-hellblau",
|
||||
name: { de: "Perlenarmband „Himmelblau”", en: "Beaded Bracelet \"Sky Blue\"", ch: "Perlenarmband „Himmelblau”", fr: "Bracelet de perles « Bleu ciel »" },
|
||||
artikelnummer: "SCH-021",
|
||||
kategorie: "modeschmuck-accessoires",
|
||||
preis: 14.5,
|
||||
bestand: 9,
|
||||
beschreibung: {
|
||||
de: "Zartes, handgefädeltes Perlenarmband mit vergoldetem Verschluss.",
|
||||
en: "A delicate, hand-strung beaded bracelet with a gold-plated clasp.",
|
||||
ch: "Zarts, vo Hang gfädlets Perlearmband mit vergoldetem Verschluss.",
|
||||
fr: "Bracelet de perles délicat, enfilé à la main, avec fermoir plaqué or.",
|
||||
},
|
||||
material: "Glasperlen, vergoldeter Edelstahl-Verschluss",
|
||||
farben: ["Hellblau", "Silber"],
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["handgemacht", "neu"],
|
||||
},
|
||||
{
|
||||
slug: "makramee-ohrringe",
|
||||
name: { de: "Makramee-Ohrringe „Terra”", en: "Macramé Earrings \"Terra\"", ch: "Makramee-Ohrringe „Terra”", fr: "Boucles d'oreilles macramé « Terra »" },
|
||||
artikelnummer: "SCH-014",
|
||||
kategorie: "modeschmuck-accessoires",
|
||||
preis: 12,
|
||||
bestand: 0,
|
||||
beschreibung: {
|
||||
de: "Leichte, geknüpfte Ohrringe aus Makramee-Garn mit Holzperle.",
|
||||
en: "Lightweight knotted earrings made from macramé cord with a wooden bead.",
|
||||
ch: "Lychti, gknüpfti Ohrringe us Makramee-Garn mit Holzperle.",
|
||||
fr: "Boucles d'oreilles légères, nouées en fil macramé, avec perle en bois.",
|
||||
},
|
||||
material: "Makramee-Garn, Holzperle, Edelstahlhaken",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["handgemacht"],
|
||||
},
|
||||
{
|
||||
slug: "haekelnadel-set-ergonomisch",
|
||||
name: { de: "Ergonomisches Häkelnadel-Set (9-teilig)", en: "Ergonomic Crochet Hook Set (9-piece)", ch: "Ergonomisches Häkelnadel-Set (9-teilig)", fr: "Set de 9 crochets ergonomiques" },
|
||||
artikelnummer: "ZUB-101",
|
||||
kategorie: "haekelzubehoer",
|
||||
preis: 22,
|
||||
bestand: 21,
|
||||
beschreibung: {
|
||||
de: "Griffige, ergonomische Häkelnadeln in 9 gängigen Stärken, inkl. Aufbewahrungsrolle.",
|
||||
en: "Comfortable, ergonomic crochet hooks in 9 common sizes, incl. a storage roll.",
|
||||
ch: "Griffigi, ergonomischi Häkelnadle i 9 gängige Stärche, inkl. Ufbewahrigsrolle.",
|
||||
fr: "Crochets ergonomiques et faciles à tenir en 9 tailles courantes, avec pochette de rangement.",
|
||||
},
|
||||
material: "Aluminium, Softgrip-Griff",
|
||||
lieferumfang: "9 Nadeln + Stoffrolle",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["bestseller"],
|
||||
},
|
||||
{
|
||||
slug: "baumwollgarn-bundle",
|
||||
name: { de: "Baumwollgarn-Bundle „Pastell” (6 Knäuel)", en: "Cotton Yarn Bundle \"Pastel\" (6 skeins)", ch: "Baumwollgarn-Bundle „Pastell” (6 Knäuel)", fr: "Lot de fil coton « Pastel » (6 pelotes)" },
|
||||
artikelnummer: "ZUB-205",
|
||||
kategorie: "haekelzubehoer",
|
||||
preis: 24.9,
|
||||
bestand: 17,
|
||||
beschreibung: {
|
||||
de: "Sechs Knäuel weiches Baumwollgarn in aufeinander abgestimmten Pastelltönen.",
|
||||
en: "Six skeins of soft cotton yarn in coordinated pastel shades.",
|
||||
ch: "Sächs Chnöiel weichs Baumwollgarn i aufenand abgstimmte Pastelltöne.",
|
||||
fr: "Six pelotes de fil coton doux dans des tons pastel assortis.",
|
||||
},
|
||||
material: "100 % Baumwolle",
|
||||
farben: ["Pastellmix"],
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["neu"],
|
||||
},
|
||||
{
|
||||
slug: "bastel-starterset",
|
||||
name: { de: "DIY Bastel-Starterset", en: "DIY Craft Starter Kit", ch: "DIY Bastel-Starterset", fr: "Kit créatif de démarrage DIY" },
|
||||
artikelnummer: "BAS-050",
|
||||
kategorie: "bastelzubehoer",
|
||||
preis: 34,
|
||||
preisAlt: 42,
|
||||
bestand: 8,
|
||||
beschreibung: {
|
||||
de: "Alles für den Einstieg: Schere, Kleber, Knöpfe, Bänder und eine kleine Anleitung mit drei Projekten.",
|
||||
en: "Everything to get started: scissors, glue, buttons, ribbon and a little guide with three projects.",
|
||||
ch: "Alles für dr Iistig: Schere, Chläber, Chnöpf, Bändel und e chlini Aaleitig mit drei Projäkt.",
|
||||
fr: "Tout pour débuter : ciseaux, colle, boutons, rubans et un petit guide avec trois projets.",
|
||||
},
|
||||
lieferumfang: "1 Set (siehe Beschreibung)",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: ["sale"],
|
||||
},
|
||||
{
|
||||
slug: "stoffreste-bundle",
|
||||
name: { de: "Stoffreste-Bundle „Bunte Mischung”", en: "Fabric Scraps Bundle \"Colorful Mix\"", ch: "Stoffreste-Bundle „Bunte Mischung”", fr: "Lot de chutes de tissu « Mélange coloré »" },
|
||||
artikelnummer: "BAS-018",
|
||||
kategorie: "bastelzubehoer",
|
||||
preis: 9.9,
|
||||
bestand: 25,
|
||||
beschreibung: {
|
||||
de: "Restestoffe in verschiedenen Mustern und Farben – ideal für kleine Nähprojekte.",
|
||||
en: "Leftover fabrics in various patterns and colors – perfect for small sewing projects.",
|
||||
ch: "Restestoff i verschiedene Muster und Farbe – ideal für chlini Nöihprojäkt.",
|
||||
fr: "Chutes de tissu aux motifs et couleurs variés – idéales pour de petits projets de couture.",
|
||||
},
|
||||
material: "Baumwollmix",
|
||||
lieferzeit: "2–4 Werktage",
|
||||
badges: [],
|
||||
},
|
||||
];
|
||||
|
||||
export function getProduct(slug: string): Product | undefined {
|
||||
return products.find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export function productsByCategory(kategorie: string): Product[] {
|
||||
return products.filter((p) => p.kategorie === kategorie);
|
||||
}
|
||||
|
||||
export function saleProducts(): Product[] {
|
||||
return products.filter((p) => p.badges.includes("sale") || p.preisAlt);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Sprach-Grundkonfiguration für Van's DIY & Bastelbedarf.
|
||||
// Deutsch ist und bleibt die Standardsprache (Startseite/Root-URLs ohne Präfix, "/de/" gibt es
|
||||
// nicht als eigene URL). EN/CH/FR leben unter /en/, /ch/, /fr/.
|
||||
// "ch" = Schweizer Hochdeutsch (kein "ß", "Strasse" statt "Straße" usw.), keine eigene Währung —
|
||||
// Van's DIY & Bastelbedarf rechnet als deutsches Kleinunternehmen weiterhin in EUR, auch für
|
||||
// Kund:innen in der Schweiz (siehe Versand & Zahlung).
|
||||
|
||||
export const locales = ["de", "en", "ch", "fr"] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
|
||||
export const defaultLocale: Locale = "de";
|
||||
|
||||
// Statt Flaggen-Emoji (rendert auf manchen Windows/Chrome-Kombinationen nur als nacktes
|
||||
// Buchstaben-Kürzel "DE"/"GB" statt als Bild) bekommt jede Sprache ein rotierendes Globus-Icon,
|
||||
// das die ECHTEN Nationalflaggen-Farben trägt (nicht erfundene Markenfarben):
|
||||
// - DE: Schwarz-Rot-Gold (Bundesflagge)
|
||||
// - EN: Rot-Weiß-Blau (Union Jack)
|
||||
// - CH: Rot-Weiß (Schweizer Kreuz)
|
||||
// - FR: Blau-Weiß-Rot (Tricolore)
|
||||
// `flag` = die 3 Streifenfarben für den rotierenden Farbverlauf im Icon.
|
||||
// `color`/`glow` = die zwei kräftigsten, auf dunklem Grund gut sichtbaren Flaggenfarben
|
||||
// (Weiß/Schwarz werden hier bewusst ausgelassen — auf dem fast schwarzen Seitenhintergrund
|
||||
// unsichtbar bzw. zu hart) — für Rand-Glow und den Farbverlauf im Sprachnamen-Schriftzug.
|
||||
export const localeMeta: Record<Locale, { label: string; color: string; glow: string; flag: [string, string, string]; htmlLang: string }> = {
|
||||
de: { label: "Deutsch", color: "#e30016", glow: "#ffce00", flag: ["#1a1a1a", "#e30016", "#ffce00"], htmlLang: "de" },
|
||||
en: { label: "English", color: "#c8102e", glow: "#3a5fcd", flag: ["#c8102e", "#ffffff", "#00247d"], htmlLang: "en" },
|
||||
ch: { label: "Schweiz", color: "#e30016", glow: "#ff6b5c", flag: ["#e30016", "#ffffff", "#e30016"], htmlLang: "de-CH" },
|
||||
fr: { label: "Français", color: "#3a5fcd", glow: "#e30016", flag: ["#0055a4", "#ffffff", "#ef4135"], htmlLang: "fr" },
|
||||
};
|
||||
|
||||
/** URL-Prefix für eine Sprache: Deutsch = kein Prefix, alle anderen = "/en" usw. */
|
||||
export function localePrefix(locale: Locale): string {
|
||||
return locale === defaultLocale ? "" : `/${locale}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus einem sprachneutralen Pfad (z.B. "/shop/" oder "/produkt/foo/") die passende URL
|
||||
* für eine Zielsprache. Immer mit führendem "/" aufrufen.
|
||||
*/
|
||||
export function localizedPath(path: string, locale: Locale): string {
|
||||
const clean = path === "/" ? "" : path;
|
||||
return `${localePrefix(locale)}${clean}` || "/";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Locale } from "./config";
|
||||
|
||||
// Echte, verifizierbare Zahlenformat-Unterschiede pro Sprache/Land (nicht nur übersetzter Text):
|
||||
// - Deutschland (de-DE): Komma als Dezimaltrennzeichen → "28,50 €"
|
||||
// - Schweiz (de-CH): Punkt als Dezimaltrennzeichen, Apostroph bei Tausendern → "28.50 €" / "1'234.50 €"
|
||||
// (die Schweiz nutzt den Punkt sogar innerhalb deutschsprachiger Texte — ein bekanntes,
|
||||
// authentisches Detail, das über reine Wortwahl hinausgeht)
|
||||
// - England/UK (en-GB): Punkt als Dezimaltrennzeichen, Symbol vor der Zahl → "€28.50"
|
||||
// - Frankreich (fr-FR): Komma als Dezimaltrennzeichen, geschütztes Leerzeichen vor dem Symbol
|
||||
//
|
||||
// Van's DIY & Bastelbedarf bleibt als deutsches Kleinunternehmen rechtlich bei EUR-Preisen
|
||||
// (keine erfundene CHF-Umrechnung) — nur die Schreibweise der Zahl selbst passt sich der
|
||||
// jeweiligen Sprachregion an.
|
||||
|
||||
const formatters: Record<Locale, Intl.NumberFormat> = {
|
||||
de: new Intl.NumberFormat("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
|
||||
en: new Intl.NumberFormat("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
|
||||
ch: new Intl.NumberFormat("de-CH", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
|
||||
fr: new Intl.NumberFormat("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
|
||||
};
|
||||
|
||||
export function formatPrice(value: number, locale: Locale = "de"): string {
|
||||
const num = (formatters[locale] ?? formatters.de).format(value);
|
||||
return locale === "en" ? `€${num}` : `${num} €`;
|
||||
}
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
import type { Locale } from "./config";
|
||||
|
||||
// Zentrales UI-Wörterbuch: Header/Footer/Buttons/wiederkehrende Bausteine + Seitentexte für alle
|
||||
// vier Sprachen (DE/EN/CH/FR). Rechtstexte (Impressum/Datenschutz/AGB/Widerruf/Muster-Widerruf)
|
||||
// bleiben bewusst nur Deutsch (siehe Vault-Notiz zu rechtlicher Genauigkeit) und stehen deshalb
|
||||
// NICHT hier drin.
|
||||
|
||||
export const ui = {
|
||||
de: {
|
||||
nav: { shop: "Shop", sale: "Sale", creative: "Über die Kreative", faq: "FAQ", contact: "Kontakt", account: "Mein Konto", login: "Anmelden" },
|
||||
topbar: "Versandkostenfrei ab 75 € innerhalb DE/AT/CH/LU",
|
||||
footer: {
|
||||
tagline: "Handgemacht mit Liebe – Kreativität zum Verschenken und Selbstbehalten.",
|
||||
shopHeading: "Shop", serviceHeading: "Service", legalHeading: "Rechtliches",
|
||||
shipping: "Versand & Zahlung", account: "Mein Konto",
|
||||
copyright: "Einzelunternehmen · Kleinunternehmer gemäß §19 UStG",
|
||||
legalNoteTitle: "Hinweis zu rechtlichen Seiten",
|
||||
legalNote: "Impressum, Datenschutz, AGB, Widerrufsbelehrung und das Muster-Widerrufsformular sind aus rechtlichen Gründen ausschließlich auf Deutsch verfügbar.",
|
||||
},
|
||||
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handgemacht", ausverkauft: "Ausverkauft" },
|
||||
common: {
|
||||
addToCart: "In den Warenkorb", soldOut: "Ausverkauft", addedToCart: "Zum Warenkorb hinzugefügt ✓",
|
||||
browseShop: "Jetzt stöbern", viewAll: "Alle ansehen →", viewAllSale: "Alle Angebote →",
|
||||
toShop: "Zum Shop →", moreAboutMe: "Mehr über mich →",
|
||||
vatNote: "Gemäß §19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.",
|
||||
allPricesPlusShipping: "Alle Preise zzgl. Versand.", plusShipping: "zzgl. Versand",
|
||||
quantity: "Anzahl", photoSoon: "Produktfoto folgt", portraitSoon: "Porträtfoto von VanVan folgt",
|
||||
handmadeBadge: "🩵 Handgemacht",
|
||||
},
|
||||
product: {
|
||||
articleNo: "Artikelnummer", material: "Material", size: "Größe", colors: "Farben",
|
||||
included: "Lieferumfang", care: "Pflegehinweise", delivery: "Lieferzeit",
|
||||
recommendations: "Das könnte dir auch gefallen",
|
||||
todoFields: "GPSR-Hinweise, Warnhinweise, Hersteller/Herstelleranschrift und Produktvideos werden ergänzt, sobald echte Produktdaten von VanVan vorliegen (Phase 2, Adminbereich).",
|
||||
photoNote: "echte Fotos ergänzt VanVan später",
|
||||
},
|
||||
shop: {
|
||||
title: "Alle Produkte", lead: "Stöbere durch alle handgemachten Stücke – filtere und sortiere nach deinen Wünschen.",
|
||||
filters: "Filter", allCategories: "Alle Kategorien", priceUpTo: "Preis bis", priceUpToValue: (v: number) => `bis ${v} €`,
|
||||
onlyAvailable: "Nur verfügbare Artikel", filterNote: "Material- und Farbfilter folgen, sobald mehr Produkte im Sortiment sind.",
|
||||
sortNew: "Neu", sortBestseller: "Bestseller", sortPriceAsc: "Preis aufsteigend", sortPriceDesc: "Preis absteigend",
|
||||
resultCount: (n: number) => `${n} Produkt${n === 1 ? "" : "e"}`, noResults: "Keine Produkte gefunden – bitte Filter anpassen.",
|
||||
breadcrumbShop: "Shop", categoryEmpty: "In dieser Kategorie sind aktuell noch keine Produkte hinterlegt – schau bald wieder vorbei.",
|
||||
categoryLabel: "Kategorie", productSingular: "Produkt", productPlural: "Produkte", sortLabel: "Sortierung", subcategoriesLabel: "Unterkategorien", subcategoryEmpty: "Für diesen Bereich stellt VanVan bald die ersten Produkte ein – schau bald wieder vorbei.",
|
||||
},
|
||||
sale: {
|
||||
eyebrow: "% Sale", title: "Aktuelle Angebote", lead: "Zeitlich begrenzte Rabatte auf ausgewählte handgemachte Stücke.",
|
||||
empty: "Aktuell läuft keine Aktion – schau bald wieder vorbei.",
|
||||
},
|
||||
faq: {
|
||||
title: "Häufig gestellte Fragen",
|
||||
items: [
|
||||
{ q: "Wie lange dauert die Lieferung?", a: "In der Regel 2–7 Werktage innerhalb Deutschlands, Österreichs, der Schweiz und Luxemburgs. Bei Sonderanfertigungen kann es etwas länger dauern – das steht dann direkt auf der Produktseite." },
|
||||
{ q: "Ab wann ist der Versand kostenlos?", a: "Ab einem Warenwert von 75 € versenden wir innerhalb der belieferten Länder kostenfrei mit DHL." },
|
||||
{ q: "Kann ich Sonderwünsche oder individuelle Anfertigungen bestellen?", a: "Ja, gerne! Schreib uns einfach über das Kontaktformular – wir schauen, was möglich ist." },
|
||||
{ q: "Wie pflege ich meine handgemachten Produkte?", a: "Die Pflegehinweise stehen auf der jeweiligen Produktseite, meist empfiehlt sich schonende Handwäsche." },
|
||||
{ q: "Welche Zahlungsarten werden akzeptiert?", a: "PayPal, Klarna und Banküberweisung." },
|
||||
{ q: "Kann ich meine Bestellung zurückgeben?", a: "Es gilt das gesetzliche Widerrufsrecht (siehe unsere Widerrufsbelehrung). Bei individuell auf dich zugeschnittenen Sonderanfertigungen kann das Widerrufsrecht gesetzlich ausgeschlossen sein." },
|
||||
{ q: "Muss ich Umsatzsteuer zahlen?", a: "Nein. Van's DIY & Bastelbedarf ist Kleinunternehmer gemäß §19 UStG – es wird keine Umsatzsteuer berechnet und ausgewiesen." },
|
||||
{ q: "Brauche ich ein Kundenkonto zum Bestellen?", a: "Nein, du kannst auch bequem als Gast bestellen. Ein Konto lohnt sich aber für Bestellhistorie und Wunschliste." },
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: "Schreib uns", lead: "Fragen zu einem Produkt, deiner Bestellung oder eine individuelle Anfrage? Melde dich gerne!",
|
||||
name: "Name", email: "E-Mail", subject: "Betreff", message: "Nachricht",
|
||||
gdprPrefix: "Ich habe die", gdprLink: "Datenschutzerklärung", gdprSuffix: "gelesen und bin mit der Verarbeitung meiner Daten einverstanden.",
|
||||
send: "Nachricht senden", sentDemo: "Danke! (Demo – noch keine echte Übertragung in Phase 1)",
|
||||
todoNote: "TODO Phase 2: Formular an eine echte Backend-/E-Mail-Anbindung (Cloudflare Worker) anschließen. Aktuell ohne echten Versand.",
|
||||
directTitle: "Direkter Kontakt", emailPlaceholderNote: "(Platzhalter-Adresse)", orderNote: "Für Bestellfragen bitte die Bestellnummer angeben.",
|
||||
responseTitle: "Antwortzeit", responseNote: "In der Regel innerhalb von 1–2 Werktagen.",
|
||||
},
|
||||
about: {
|
||||
eyebrow: "Über die Kreative", title: "Hallo, ich bin VanVan 🩵",
|
||||
lead: "Willkommen in meiner kleinen Werkstatt! Hier entsteht alles, was du im Shop findest, von Hand – Stich für Stich, Perle für Perle.",
|
||||
valuesTitle: "Was mir wichtig ist",
|
||||
values: [
|
||||
{ icon: "🧵", title: "Handarbeit", text: "Kein Massenprodukt – jedes Stück entsteht einzeln und mit Sorgfalt." },
|
||||
{ icon: "🩵", title: "Qualität", text: "Sorgfältig ausgewählte Materialien für langlebige, schöne Ergebnisse." },
|
||||
{ icon: "🎁", title: "Persönlich", text: "Ob Geschenk oder Selbstbehalten – jedes Produkt trägt ein Stück Herzblut." },
|
||||
],
|
||||
story: [
|
||||
{
|
||||
title: "Meine Reise begann mit einer einzigen Masche",
|
||||
paragraphs: [
|
||||
"Vor etwa sechs Jahren habe ich meine Leidenschaft für das Häkeln entdeckt. Angefangen hat alles aus reiner Neugier. Ohne Kurse oder Vorkenntnisse habe ich mir Schritt für Schritt alles selbst beigebracht.",
|
||||
"Mit jeder neuen Masche, jedem kleinen Fehler und jedem fertigen Projekt wuchs nicht nur mein Können, sondern auch meine Begeisterung für dieses wundervolle Handwerk.",
|
||||
"Heute ist das Häkeln für mich weit mehr als nur ein Hobby. Es ist mein kreativer Ausgleich, eine Möglichkeit, Ideen Wirklichkeit werden zu lassen und Menschen mit liebevoll gestalteten Unikaten ein Lächeln ins Gesicht zu zaubern.",
|
||||
"Besonders die Herstellung von Amigurumis begeistert mich immer wieder aufs Neue. Es fasziniert mich, wie aus einem einfachen Knäuel Wolle nach und nach kleine Persönlichkeiten entstehen – jede einzelne mit ihrem ganz eigenen Charme.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Kreativität kennt keine Grenzen",
|
||||
paragraphs: [
|
||||
"Neben dem Häkeln habe ich auch meine Leidenschaft für handgefertigten Modeschmuck entdeckt.",
|
||||
"Mit viel Sorgfalt fertige ich Armbänder, Halsketten, Schlüsselanhänger und weitere Accessoires an. Dabei liebe ich es, verschiedene Materialien, Farben und Anhänger miteinander zu kombinieren und immer wieder neue Designs entstehen zu lassen.",
|
||||
"Jedes Schmuckstück soll nicht nur schön aussehen, sondern seinem neuen Besitzer lange Freude bereiten.",
|
||||
"Kreativität begleitet mich schon mein ganzes Leben. Deshalb findest du bei mir nicht nur klassische Häkelarbeiten oder Schmuck, sondern immer wieder neue kreative Ideen und Projekte, die mit viel Liebe entstehen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Qualität mit Herz",
|
||||
paragraphs: [
|
||||
"Für mich bedeutet Handarbeit Persönlichkeit.",
|
||||
"Kein Produkt entsteht am Fließband.",
|
||||
"Jedes einzelne Stück wird von mir sorgfältig geplant, gefertigt und kontrolliert.",
|
||||
"Dabei lege ich besonderen Wert auf hochwertige Materialien, saubere Verarbeitung, liebevolle Details, langlebige Qualität und einzigartige Designs.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Mehr als nur ein Hobby",
|
||||
paragraphs: [
|
||||
"Hinter meiner Kreativität steckt weit mehr als Wolle und Perlen.",
|
||||
"Ich bin ein Mensch, der mit ganzem Herzen bei der Sache ist. Wenn ich etwas tue, dann mit Leidenschaft, Geduld und dem Wunsch, etwas Besonderes zu erschaffen.",
|
||||
"Es macht mich glücklich, wenn meine Werke anderen Menschen Freude schenken – sei es als Geschenk für einen lieben Menschen oder einfach als kleine Aufmerksamkeit für sich selbst.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Mein kleiner Herzensshop",
|
||||
paragraphs: [
|
||||
"Mein Shop ist deshalb nicht einfach nur ein Ort, an dem Produkte verkauft werden.",
|
||||
"Er ist ein Teil von mir.",
|
||||
"Hier stecken unzählige Stunden Arbeit, Kreativität, Herzblut und ganz viel Liebe in jedem einzelnen Werk.",
|
||||
"Genau das macht jedes Stück einzigartig.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Danke, dass du da bist",
|
||||
paragraphs: [
|
||||
"Vielen Dank, dass du dir die Zeit nimmst, meine kleine kreative Welt kennenzulernen und meine Leidenschaft zu unterstützen.",
|
||||
"Ich wünsche dir ganz viel Freude beim Stöbern und hoffe, dass du genau das eine Stück findest, das dein Herz höherschlagen lässt.",
|
||||
],
|
||||
},
|
||||
],
|
||||
closing: "Mit Liebe handgemacht – Masche für Masche und Perle für Perle.",
|
||||
},
|
||||
shipping: {
|
||||
eyebrow: "Versand & Zahlung", title: "Versand & Zahlung",
|
||||
shippingTitle: "📦 Versand",
|
||||
shippingItems: [
|
||||
"Versand ausschließlich mit DHL", "Lieferländer: Deutschland, Österreich, Schweiz, Luxemburg",
|
||||
"Lieferzeit: 2–7 Werktage", "Versandkosten sind pro Produkt hinterlegt und zusätzlich nach Warenwert & Land gestaffelt",
|
||||
"Versandkostenfrei ab 75 € Warenwert", "Der Versand erfolgt erst nach Zahlungseingang",
|
||||
],
|
||||
paymentTitle: "💳 Zahlungsarten", paymentItems: ["PayPal", "Klarna", "Banküberweisung (Vorkasse)"],
|
||||
todoNote: "Konkrete Versandkosten-Staffel je Land/Warenwert wird ergänzt, sobald VanVan die finalen DHL-Konditionen festgelegt hat.",
|
||||
},
|
||||
cart: {
|
||||
eyebrow: "Warenkorb", title: "Dein Warenkorb", empty: "Dein Warenkorb ist noch leer.",
|
||||
subtotal: "Zwischensumme", shipping: "Versand", shippingCalculated: "wird im Checkout berechnet",
|
||||
total: "Gesamt", remaining: (v: string) => `Noch ${v} bis zum kostenlosen Versand.`, freeShipping: "🩵 Du bekommst kostenlosen Versand!",
|
||||
toCheckout: "Weiter zum Checkout", remove: "Entfernen", perItem: "/ Stück",
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Bestellung abschließen",
|
||||
todoNote: "Phase 1 – Demo-Checkout. Es findet noch keine echte Zahlung statt. Die vollständige Anbindung an PayPal, Klarna und Banküberweisung sowie die Bestellverwaltung folgen in Phase 2 (Cloudflare Worker + D1, siehe Tech-Stack-Entscheidung im Projekt-Vault).",
|
||||
step1: "1. Kontakt & Lieferadresse", email: "E-Mail", firstName: "Vorname", lastName: "Nachname",
|
||||
street: "Straße & Hausnummer", zip: "PLZ", city: "Ort", country: "Land",
|
||||
step2: "2. Zahlungsart",
|
||||
step3: "3. Bestellübersicht", agbPrefix: "Ich habe die", agbLink: "AGB", agbSuffix: "gelesen und akzeptiere sie.",
|
||||
revocationPrefix: "Ich habe die", revocationLink: "Widerrufsbelehrung", revocationAnd: "und",
|
||||
privacyLink: "Datenschutzerklärung", revocationSuffix: "zur Kenntnis genommen.",
|
||||
bankTransfer: "Banküberweisung (Vorkasse)", orderButton: "Jetzt zahlungspflichtig bestellen", emptyCartAlert: "Dein Warenkorb ist leer.",
|
||||
shippingHintTitle: "Versandhinweis",
|
||||
shippingHint: "Versand ausschließlich per DHL nach Deutschland, Österreich, Schweiz und Luxemburg. Lieferzeit 2–7 Werktage. Versandkostenfrei ab 75 €. Versand erst nach Zahlungseingang.",
|
||||
shippingLink: "Mehr zu Versand & Zahlung →",
|
||||
thankYouEyebrow: "Danke", thankYouTitle: "Deine Bestellung ist eingegangen",
|
||||
thankYouLead: "Das ist aktuell eine Demo-Bestätigung (Phase 1, ohne echte Zahlungsabwicklung). Sobald der echte Checkout live ist, bekommst du hier und per E-Mail eine richtige Bestellbestätigung.",
|
||||
continueShopping: "Weiter stöbern", total: "Gesamt", free: "kostenlos",
|
||||
},
|
||||
account: {
|
||||
eyebrow: "Mein Konto", title: "Anmelden oder als Gast bestellen",
|
||||
todoNote: "Phase 1 – reine Oberfläche ohne echtes Login/Registrierung. Bestellhistorie, Sendungsverfolgung, Wunschliste und \"Zuletzt angesehen\" folgen in Phase 2 mit echter Datenbank-Anbindung.",
|
||||
loginTitle: "Anmelden", email: "E-Mail", password: "Passwort", loginButton: "Anmelden (folgt in Phase 2)", forgotPassword: "Passwort vergessen?",
|
||||
newTitle: "Neu hier?", newText: "Lege ein Konto an, um Bestellhistorie, Wunschliste und Sendungsverfolgung an einem Ort zu haben – oder bestelle einfach als Gast, ganz ohne Konto.",
|
||||
guestButton: "Als Gast weiter einkaufen",
|
||||
},
|
||||
},
|
||||
en: {
|
||||
nav: { shop: "Shop", sale: "Sale", creative: "About the Maker", faq: "FAQ", contact: "Contact", account: "My Account", login: "Sign in" },
|
||||
topbar: "Free shipping from €75 within DE/AT/CH/LU",
|
||||
footer: {
|
||||
tagline: "Handmade with love – creativity to gift and to keep.",
|
||||
shopHeading: "Shop", serviceHeading: "Service", legalHeading: "Legal",
|
||||
shipping: "Shipping & Payment", account: "My Account",
|
||||
copyright: "Sole proprietorship · Small business per §19 UStG (German VAT law)",
|
||||
legalNoteTitle: "Note on legal pages",
|
||||
legalNote: "Imprint, privacy policy, terms & conditions, withdrawal notice and the sample withdrawal form are, for legal accuracy, only available in German.",
|
||||
},
|
||||
badge: { neu: "New", bestseller: "Bestseller", sale: "Sale", handgemacht: "Handmade", ausverkauft: "Sold out" },
|
||||
common: {
|
||||
addToCart: "Add to cart", soldOut: "Sold out", addedToCart: "Added to cart ✓",
|
||||
browseShop: "Browse now", viewAll: "View all →", viewAllSale: "View all offers →",
|
||||
toShop: "To the shop →", moreAboutMe: "More about me →",
|
||||
vatNote: "Under §19 UStG (German small-business rule) no VAT is charged or shown.",
|
||||
allPricesPlusShipping: "All prices plus shipping.", plusShipping: "plus shipping",
|
||||
quantity: "Quantity", photoSoon: "Product photo coming soon", portraitSoon: "VanVan's portrait photo coming soon",
|
||||
handmadeBadge: "🩵 Handmade",
|
||||
},
|
||||
product: {
|
||||
articleNo: "Item number", material: "Material", size: "Size", colors: "Colors",
|
||||
included: "What's included", care: "Care instructions", delivery: "Delivery time",
|
||||
recommendations: "You might also like",
|
||||
todoFields: "GPSR notices, warnings, manufacturer details and product videos will be added once VanVan provides real product data (Phase 2, admin area).",
|
||||
photoNote: "real photos will be added by VanVan later",
|
||||
},
|
||||
shop: {
|
||||
title: "All products", lead: "Browse all handmade pieces – filter and sort to your liking.",
|
||||
filters: "Filters", allCategories: "All categories", priceUpTo: "Price up to", priceUpToValue: (v: number) => `up to €${v}`,
|
||||
onlyAvailable: "In-stock items only", filterNote: "Material and color filters will follow once more products are in the shop.",
|
||||
sortNew: "New", sortBestseller: "Bestseller", sortPriceAsc: "Price: low to high", sortPriceDesc: "Price: high to low",
|
||||
resultCount: (n: number) => `${n} product${n === 1 ? "" : "s"}`, noResults: "No products found – please adjust your filters.",
|
||||
breadcrumbShop: "Shop", categoryEmpty: "No products in this category yet – check back soon.",
|
||||
categoryLabel: "Category", productSingular: "product", productPlural: "products", sortLabel: "Sort by", subcategoriesLabel: "Subcategories", subcategoryEmpty: "VanVan will be adding the first products here soon – check back shortly.",
|
||||
},
|
||||
sale: {
|
||||
eyebrow: "% Sale", title: "Current offers", lead: "Time-limited discounts on selected handmade pieces.",
|
||||
empty: "No promotion is running at the moment – check back soon.",
|
||||
},
|
||||
faq: {
|
||||
title: "Frequently Asked Questions",
|
||||
items: [
|
||||
{ q: "How long does delivery take?", a: "Usually 2–7 business days within Germany, Austria, Switzerland and Luxembourg. Custom pieces may take a little longer – noted directly on the product page." },
|
||||
{ q: "From when is shipping free?", a: "From an order value of €75 we ship free of charge via DHL within the countries we deliver to." },
|
||||
{ q: "Can I request custom or personalized items?", a: "Yes, gladly! Just reach out via the contact form – we'll see what's possible." },
|
||||
{ q: "How do I care for my handmade items?", a: "Care instructions are listed on the respective product page, usually gentle hand-washing is recommended." },
|
||||
{ q: "Which payment methods are accepted?", a: "PayPal, Klarna and bank transfer." },
|
||||
{ q: "Can I return my order?", a: "The statutory right of withdrawal applies (see our withdrawal notice). For custom-made pieces tailored to you, the right of withdrawal may be excluded by law." },
|
||||
{ q: "Do I have to pay VAT?", a: "No. Van's DIY & Bastelbedarf is a small business per §19 UStG – no VAT is charged or shown." },
|
||||
{ q: "Do I need an account to order?", a: "No, you can conveniently check out as a guest. An account is worthwhile for order history and your wishlist though." },
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: "Get in touch", lead: "Questions about a product, your order, or a custom request? Feel free to reach out!",
|
||||
name: "Name", email: "Email", subject: "Subject", message: "Message",
|
||||
gdprPrefix: "I have read the", gdprLink: "privacy policy", gdprSuffix: "and agree to the processing of my data.",
|
||||
send: "Send message", sentDemo: "Thank you! (Demo – no real submission yet in Phase 1)",
|
||||
todoNote: "TODO Phase 2: connect this form to a real backend/email service (Cloudflare Worker). Currently no real sending happens.",
|
||||
directTitle: "Direct contact", emailPlaceholderNote: "(placeholder address)", orderNote: "Please include your order number for order-related questions.",
|
||||
responseTitle: "Response time", responseNote: "Usually within 1–2 business days.",
|
||||
},
|
||||
about: {
|
||||
eyebrow: "About the Maker", title: "Hi, I'm VanVan 🩵",
|
||||
lead: "Welcome to my little workshop! Everything you find in the shop is made here by hand – stitch by stitch, bead by bead.",
|
||||
valuesTitle: "What matters to me",
|
||||
values: [
|
||||
{ icon: "🧵", title: "Craftsmanship", text: "No mass production – every piece is made individually and with care." },
|
||||
{ icon: "🩵", title: "Quality", text: "Carefully chosen materials for durable, beautiful results." },
|
||||
{ icon: "🎁", title: "Personal", text: "Whether a gift or a treat for yourself – every product carries a piece of heart." },
|
||||
],
|
||||
story: [
|
||||
{
|
||||
title: "My journey began with a single stitch",
|
||||
paragraphs: [
|
||||
"About six years ago, I discovered my passion for crochet. It all started out of pure curiosity. Without any courses or prior experience, I taught myself everything step by step.",
|
||||
"With every new stitch, every small mistake and every finished project, not only my skills grew — so did my enthusiasm for this wonderful craft.",
|
||||
"Today, crochet is far more than just a hobby to me. It's my creative outlet, a way to turn ideas into reality and to bring a smile to people's faces with lovingly made one-of-a-kind pieces.",
|
||||
"I especially love making amigurumis. I'm fascinated by how a simple ball of yarn gradually turns into a little character — each one with its own unique charm.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Creativity knows no limits",
|
||||
paragraphs: [
|
||||
"Alongside crochet, I also discovered my passion for handmade jewelry.",
|
||||
"With great care, I make bracelets, necklaces, keychains and other accessories. I love combining different materials, colors and charms to create new designs again and again.",
|
||||
"Every piece of jewelry should not only look beautiful, but bring its new owner joy for a long time to come.",
|
||||
"Creativity has been with me my whole life. That's why you'll find more at my shop than just classic crochet work or jewelry — always new creative ideas and projects, made with a lot of love.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Quality with heart",
|
||||
paragraphs: [
|
||||
"To me, handmade means personality.",
|
||||
"No product comes off an assembly line.",
|
||||
"Every single piece is carefully planned, crafted and checked by me.",
|
||||
"I place special value on high-quality materials, clean workmanship, loving details, lasting quality and unique designs.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "More than just a hobby",
|
||||
paragraphs: [
|
||||
"Behind my creativity lies far more than yarn and beads.",
|
||||
"I'm someone who puts her whole heart into what she does. When I do something, I do it with passion, patience and the wish to create something special.",
|
||||
"It makes me happy when my work brings joy to others — whether as a gift for someone special or as a little treat for yourself.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "My little heart-shop",
|
||||
paragraphs: [
|
||||
"That's why my shop isn't just a place where products are sold.",
|
||||
"It's a part of me.",
|
||||
"Countless hours of work, creativity, heart and so much love go into every single piece here.",
|
||||
"That's exactly what makes each piece unique.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Thank you for being here",
|
||||
paragraphs: [
|
||||
"Thank you so much for taking the time to get to know my little creative world and for supporting my passion.",
|
||||
"I wish you lots of joy browsing, and I hope you find exactly the one piece that makes your heart skip a beat.",
|
||||
],
|
||||
},
|
||||
],
|
||||
closing: "Handmade with love – stitch by stitch and bead by bead.",
|
||||
},
|
||||
shipping: {
|
||||
eyebrow: "Shipping & Payment", title: "Shipping & Payment",
|
||||
shippingTitle: "📦 Shipping",
|
||||
shippingItems: [
|
||||
"Shipped exclusively via DHL", "Countries we deliver to: Germany, Austria, Switzerland, Luxembourg",
|
||||
"Delivery time: 2–7 business days", "Shipping costs are set per product and further tiered by order value & country",
|
||||
"Free shipping from €75 order value", "Shipping only starts after payment has been received",
|
||||
],
|
||||
paymentTitle: "💳 Payment methods", paymentItems: ["PayPal", "Klarna", "Bank transfer (prepayment)"],
|
||||
todoNote: "Concrete shipping-cost tiers per country/order value will be added once VanVan finalizes the DHL terms.",
|
||||
},
|
||||
cart: {
|
||||
eyebrow: "Cart", title: "Your cart", empty: "Your cart is still empty.",
|
||||
subtotal: "Subtotal", shipping: "Shipping", shippingCalculated: "calculated at checkout",
|
||||
total: "Total", remaining: (v: string) => `${v} more for free shipping.`, freeShipping: "🩵 You qualify for free shipping!",
|
||||
toCheckout: "Continue to checkout", remove: "Remove", perItem: "/ item",
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Complete your order",
|
||||
todoNote: "Phase 1 – demo checkout. No real payment is processed yet. Full integration with PayPal, Klarna and bank transfer, plus order management, follow in Phase 2 (Cloudflare Worker + D1, see the tech-stack decision in the project vault).",
|
||||
step1: "1. Contact & shipping address", email: "Email", firstName: "First name", lastName: "Last name",
|
||||
street: "Street & house number", zip: "ZIP code", city: "City", country: "Country",
|
||||
step2: "2. Payment method",
|
||||
step3: "3. Order summary", agbPrefix: "I have read the", agbLink: "terms & conditions", agbSuffix: "and accept them.",
|
||||
revocationPrefix: "I have taken note of the", revocationLink: "withdrawal notice", revocationAnd: "and",
|
||||
privacyLink: "privacy policy", revocationSuffix: ".",
|
||||
bankTransfer: "Bank transfer (prepayment)", orderButton: "Order now, subject to payment", emptyCartAlert: "Your cart is empty.",
|
||||
shippingHintTitle: "Shipping note",
|
||||
shippingHint: "Shipped exclusively via DHL to Germany, Austria, Switzerland and Luxembourg. Delivery time 2–7 business days. Free shipping from €75. Shipping only starts after payment has been received.",
|
||||
shippingLink: "More on shipping & payment →",
|
||||
thankYouEyebrow: "Thank you", thankYouTitle: "Your order has been received",
|
||||
thankYouLead: "This is currently a demo confirmation (Phase 1, no real payment processing). Once the real checkout is live, you'll receive a proper order confirmation here and by email.",
|
||||
continueShopping: "Continue browsing", total: "Total", free: "free",
|
||||
},
|
||||
account: {
|
||||
eyebrow: "My Account", title: "Sign in or check out as a guest",
|
||||
todoNote: "Phase 1 – interface only, no real login/registration yet. Order history, tracking, wishlist and \"recently viewed\" follow in Phase 2 with a real database connection.",
|
||||
loginTitle: "Sign in", email: "Email", password: "Password", loginButton: "Sign in (coming in Phase 2)", forgotPassword: "Forgot password?",
|
||||
newTitle: "New here?", newText: "Create an account to keep order history, wishlist and tracking in one place – or simply check out as a guest, no account needed.",
|
||||
guestButton: "Continue shopping as guest",
|
||||
},
|
||||
},
|
||||
ch: {
|
||||
nav: { shop: "Shop", sale: "Aktion", creative: "Über d'Kreativi", faq: "FAQ", contact: "Kontakt", account: "Mis Konto", login: "Aamälde" },
|
||||
topbar: "Gratisversand ab 75 € innerhalb DE/AT/CH/LU",
|
||||
footer: {
|
||||
tagline: "Vo Hang gmacht mit Härz – Sächeli zum Verschänke und Sälber Bhalte.",
|
||||
shopHeading: "Shop", serviceHeading: "Service", legalHeading: "Rechtligs",
|
||||
shipping: "Versand & Zahlig", account: "Mis Konto",
|
||||
copyright: "Einzelunternehmen · Kleinunternehmer gmäss §19 UStG (dütsches Rächt)",
|
||||
legalNoteTitle: "Hinwys zu de rächtliche Syte",
|
||||
legalNote: "Impressum, Datenschutz, AGB, Widerrufsbelehrig und s'Muster-Widerrufsformular gits us rächtliche Gründ nur uf Dütsch.",
|
||||
},
|
||||
badge: { neu: "Neu", bestseller: "Bestseller", sale: "Aktion", handgemacht: "Handgmacht", ausverkauft: "Usverkauft" },
|
||||
common: {
|
||||
addToCart: "In Warenchorb", soldOut: "Usverkauft", addedToCart: "Zum Warenchorb dezuegfüegt ✓",
|
||||
browseShop: "Jetzt use luege", viewAll: "Alles aluege →", viewAllSale: "Alli Aktione →",
|
||||
toShop: "Zum Shop →", moreAboutMe: "Meh über mich →",
|
||||
vatNote: "Gemäss §19 UStG (dütsches Rächt) wird kei Umsatzsteuer berechnet und usgwiese.",
|
||||
allPricesPlusShipping: "Alli Pryse zzgl. Versand.", plusShipping: "zzgl. Versand",
|
||||
quantity: "Aazahl", photoSoon: "Produktfoto chunnt bald", portraitSoon: "Porträtfoto vo VanVan chunnt bald",
|
||||
handmadeBadge: "🩵 Vo Hang gmacht",
|
||||
},
|
||||
product: {
|
||||
articleNo: "Artikelnummer", material: "Material", size: "Grösse", colors: "Farbe",
|
||||
included: "Lieferumfang", care: "Pflegehinwys", delivery: "Lieferzyt",
|
||||
recommendations: "Das chönnt dir au gfalle",
|
||||
todoFields: "GPSR-Hinwys, Warnhinwys, Hersteller/Herstelleradrässe und Produktvideo werded ergänzt, sobald echti Produktdate vo VanVan da sind (Phase 2, Adminbereich).",
|
||||
photoNote: "echti Fotis macht VanVan später drby",
|
||||
},
|
||||
shop: {
|
||||
title: "Alli Produkt", lead: "Lueg dr alli vo Hang gmachte Sächeli a – filtere und sortiere, wie's dir passt.",
|
||||
filters: "Filter", allCategories: "Alli Kategorie", priceUpTo: "Pris bis", priceUpToValue: (v: number) => `bis ${v} €`,
|
||||
onlyAvailable: "Nur wo grad a Lager isch", filterNote: "Material- und Farbfilter chöme, sobald meh Produkt im Sortiment sind.",
|
||||
sortNew: "Neu", sortBestseller: "Bestseller", sortPriceAsc: "Pris ufsteigend", sortPriceDesc: "Pris absteigend",
|
||||
resultCount: (n: number) => `${n} Produkt`, noResults: "Kei Produkt gfunde – bitte Filter apasse.",
|
||||
breadcrumbShop: "Shop", categoryEmpty: "In däre Kategorie sind grad no kei Produkt hinterlegt – lueg spöter nomal verbi.",
|
||||
categoryLabel: "Kategorie", productSingular: "Produkt", productPlural: "Produkt", sortLabel: "Sortiere", subcategoriesLabel: "Unterkategorie", subcategoryEmpty: "Für das da stellt VanVan bald die erschte Produkt i – lueg bald wieder verbi.",
|
||||
},
|
||||
sale: {
|
||||
eyebrow: "% Aktion", title: "Aktuelli Aktion", lead: "Zytlich begrenzti Rabatt uf uswählti, vo Hang gmachti Sächeli.",
|
||||
empty: "Momentan lauft käni Aktion – lueg spöter nomal verbi.",
|
||||
},
|
||||
faq: {
|
||||
title: "Häufigi Frage",
|
||||
items: [
|
||||
{ q: "Wie lang geits bis d'Lieferig chunnt?", a: "Normalerwiis 2–7 Werktäg innerhalb Dütschland, Öschterrych, dr Schwiz und Luxemburg. Bi Sondranfertigunge cha's chli länger gah – das staht denn direkt uf dr Produktsyte." },
|
||||
{ q: "Ab wenn isch dr Versand gratis?", a: "Ab emene Warewärt vo 75 € schicke mir innerhalb vo de belieferte Länder gratis mit DHL." },
|
||||
{ q: "Cha ich Sonderwünsch oder öppis Individuells bstelle?", a: "Ja, gärn! Schrib eus eifach übers Kontaktformular – mir luege, was geit." },
|
||||
{ q: "Wie pfläg ich mini vo Hang gmachte Sächeli?", a: "D'Pflegehinwys stöh uf dr jewylige Produktsyte, meischtens isch schonendi Handwösch am beschte." },
|
||||
{ q: "Weli Zahligsarte werded akzeptiert?", a: "PayPal, Klarna und Banküberwysig." },
|
||||
{ q: "Cha ich mini Bschtellig zrugg gäh?", a: "Es gilt das gsetzlichi Widerrufsrächt (lueg üsi Widerrufsbelehrig aa). Bi Sondranfertigunge, wo speziell uf dich gmacht sind, cha das Widerrufsrächt gsetzlich usgschlosse si." },
|
||||
{ q: "Muess ich Umsatzsteuer zahle?", a: "Nei. Van's DIY & Bastelbedarf isch Kleinunternehmer gemäss §19 UStG – es wird kei Umsatzsteuer berechnet und usgwiese." },
|
||||
{ q: "Bruuch ich es Kundekonto zum Bstelle?", a: "Nei, du chasch au eifach als Gast bstelle. Es Konto lohnt sich aber für d'Bstellhistorie und d'Wunschlischte." },
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: "Schrib eus", lead: "Hesch Frage zu emene Produkt, dinere Bschtellig oder öppis Spezielles? Mäld dich gärn!",
|
||||
name: "Name", email: "E-Mail", subject: "Betreff", message: "Nachricht",
|
||||
gdprPrefix: "Ich ha d'", gdprLink: "Datenschutzerklärig", gdprSuffix: "gläse und bin mit dr Verarbeitig vo mine Date iverstande.",
|
||||
send: "Nachricht abschicke", sentDemo: "Merci! (Demo – noch keine echte Übertragung in Phase 1)",
|
||||
todoNote: "TODO Phase 2: Formular a e echti Backend-/E-Mail-Aabindig (Cloudflare Worker) aaschliesse. Momentan ohni echte Versand.",
|
||||
directTitle: "Direkte Kontaktdate", emailPlaceholderNote: "(Platzhalter-Adrässe)", orderNote: "Für Bstellfrage bitte d'Bstellnummere aagäh.",
|
||||
responseTitle: "Antwortzyt", responseNote: "Meistens innerhalb vo 1–2 Werktage.",
|
||||
},
|
||||
about: {
|
||||
eyebrow: "Über d'Kreativi", title: "Hoi zäme, ich bin VanVan 🩵",
|
||||
lead: "Willkomme i minere chline Werkstatt! Da entsteit alles, wo du im Shop findsch, vo Hang – Stich für Stich, Perle für Perle.",
|
||||
valuesTitle: "Was mir wichtig isch",
|
||||
values: [
|
||||
{ icon: "🧵", title: "Handarbeit", text: "Kes Masseprodukt – jedes Sächeli wird einzeln und mit Sorgfalt gmacht." },
|
||||
{ icon: "🩵", title: "Qualität", text: "Sorgfältig uswählti Materialie für hebigi, schöni Ergebnis." },
|
||||
{ icon: "🎁", title: "Persönlich", text: "Öb Gschänk oder für dich sälber – jedes Produkt treit es Stückli Härzbluet." },
|
||||
],
|
||||
story: [
|
||||
{
|
||||
title: "Mini Reis het aafange mit-ere einzige Masche",
|
||||
paragraphs: [
|
||||
"Vor öppe sächs Jahr han ich mini Leidenschaft fürs Häkle entdeckt. Aagfange het alles us purer Neugier. Ohni Kurs oder Vorchänntnis han ich mer Schritt für Schritt alles sälber bybracht.",
|
||||
"Mit jeder neue Masche, jedem chline Fähler und jedem fertige Projekt isch nid nur mis Chönne gwachsen, sondern au mini Begeisterig für das wundervolle Handwerch.",
|
||||
"Hüt isch Häkle für mich viu meh als nur es Hobby. Es isch min kreative Usgliich, e Möglichkeit, Idee Wirklichkeit werde z'lah und Lüt mit liebevoll gstaltete Unikate es Lächle is Gsicht z'zaubere.",
|
||||
"Bsungers d'Herstellig vo Amigurumis begeistert mich geng wieder vo Neuem. Es fasziniert mich, wie us emene eifache Chnaul Wolle nach und nach chlini Persönlichkeite entstöh – jedi einzelni mit ihrem ganz eigene Charme.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Kreativität känt kei Grenze",
|
||||
paragraphs: [
|
||||
"Näben em Häkle han ich au mini Leidenschaft für sälber gmachte Modeschmuck entdeckt.",
|
||||
"Mit viu Sorgfalt fertig ich Armbändel, Halschette, Schlüsselaahänger und witeri Accessoires a. Debi ha ich's gärn, verschiedeni Materialie, Farbe und Aahänger mitenand z'kombiniere und geng wieder neui Designs entstöh z'lah.",
|
||||
"Jedes Schmuckstück söll nid nur schön uszgseh, sondern sim neue Bsitzer lang Freud bringe.",
|
||||
"Kreativität begleitet mich scho mis ganze Läbe. Drum findsch bi mir nid nur klassischi Häkelarbeite oder Schmuck, sondern geng wieder neui kreativi Idee und Projäkt, wo mit viu Liebi entstöh.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Qualität mit Härz",
|
||||
paragraphs: [
|
||||
"Für mich bedütet Handarbeit Persönlichkeit.",
|
||||
"Kes Produkt entsteit am Fliessband.",
|
||||
"Jedes einzelne Stück wird vo mir sorgfältig plant, gfertigt und kontrolliert.",
|
||||
"Debi leg ich bsundere Wärt uf hochwärtigi Materialie, suberi Verarbeitig, liebevolli Detail, hebigi Qualität und einzigartigi Designs.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Meh als nur es Hobby",
|
||||
paragraphs: [
|
||||
"Hinter minere Kreativität steckt viu meh als Wolle und Perle.",
|
||||
"Ich bi e Mensch, wo mit ganzem Härz debi isch. Wenn ich öppis mach, denn mit Leidenschaft, Geduld und em Wunsch, öppis Bsunders z'erschaffe.",
|
||||
"Es mached mich glücklich, wenn mini Wärch andere Lüt Freud schänke – sig's als Gschänk für en liebe Mensch oder eifach als chlini Ufmerksamkeit für sich sälber.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Min chline Härzens-Lade",
|
||||
paragraphs: [
|
||||
"Min Lade isch drum nid eifach nur en Ort, wo Produkt verchauft werde.",
|
||||
"Er isch en Teil vo mir.",
|
||||
"Da steckn unzählegi Stund Arbet, Kreativität, Härzbluet und ganz viu Liebi i jedem einzelne Wärch.",
|
||||
"Genau das mached jedes Stück einzigartig.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Merci, dass du da bisch",
|
||||
paragraphs: [
|
||||
"Merci vilmal, dass du dir d'Zyt nimmsch, mini chlini kreativi Wält kenne z'lehre und mini Leidenschaft z'unterstütze.",
|
||||
"Ich wünsche dir ganz viu Freud bim Stöbere und hoffe, dass du genau das eint Stück findsch, wo dis Härz höcher schla lat.",
|
||||
],
|
||||
},
|
||||
],
|
||||
closing: "Mit Liebi vo Hang gmacht – Masche für Masche und Perle für Perle.",
|
||||
},
|
||||
shipping: {
|
||||
eyebrow: "Versand & Zahlig", title: "Versand & Zahlig",
|
||||
shippingTitle: "📦 Versand",
|
||||
shippingItems: [
|
||||
"Versand nume mit DHL", "Lieferländer: Dütschland, Öschterrych, d'Schwiz, Luxemburg",
|
||||
"Lieferzyt: 2–7 Werktäg", "Versandchoschte sind pro Produkt hinterlegt und zuesätzlich nach Warewärt & Land gstaffelt",
|
||||
"Gratisversand ab 75 € Warewärt", "Dr Versand geit erscht los, wenn d'Zahlig igange isch",
|
||||
],
|
||||
paymentTitle: "💳 Zahligsarte", paymentItems: ["PayPal", "Klarna", "Banküberwysig (Vorusszahlig)"],
|
||||
todoNote: "Di gnaui Versandchoschte-Staffle je Land/Warewärt wird ergänzt, sobald VanVan di definitive DHL-Konditione festgleit hät.",
|
||||
},
|
||||
cart: {
|
||||
eyebrow: "Warenchorb", title: "Din Warenchorb", empty: "Din Warenchorb isch no leer.",
|
||||
subtotal: "Zwüschesumme", shipping: "Versand", shippingCalculated: "wird im Checkout berechnet",
|
||||
total: "Total", remaining: (v: string) => `No ${v} bis zum Gratisversand.`, freeShipping: "🩵 Du übercho't Gratisversand!",
|
||||
toCheckout: "Wyter zum Checkout", remove: "Ewägmache", perItem: "/ Stuck",
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Checkout", title: "Bschtellig abschliesse",
|
||||
todoNote: "Phase 1 – Demo-Checkout. Es git no kei echti Zahlig. D'vollständigi Aabindig a PayPal, Klarna und Banküberwysig sowie d'Bstellverwaltig chöme i Phase 2.",
|
||||
step1: "1. Kontakt & Lieferadrässe", email: "E-Mail", firstName: "Vorname", lastName: "Nachname",
|
||||
street: "Strasse & Huusnummere", zip: "PLZ", city: "Ort", country: "Land",
|
||||
step2: "2. Zahligsart",
|
||||
step3: "3. Bschtellübersicht", agbPrefix: "Ich ha d'", agbLink: "AGB", agbSuffix: "gläse und akzeptiere si.",
|
||||
revocationPrefix: "Ich ha d'", revocationLink: "Widerrufsbelehrig", revocationAnd: "und d'",
|
||||
privacyLink: "Datenschutzerklärig", revocationSuffix: "zur Kenntnis gnoh.",
|
||||
bankTransfer: "Banküberwysig (Vorusszahlig)", orderButton: "Jetzt zahligspflichtig bstelle", emptyCartAlert: "Din Warenchorb isch leer.",
|
||||
shippingHintTitle: "Versandhinwys",
|
||||
shippingHint: "Versand nume per DHL nach Dütschland, Öschterrych, d'Schwiz und Luxemburg. Lieferzyt 2–7 Werktäg. Gratisversand ab 75 €. Dr Versand geit erscht los, wenn d'Zahlig igange isch.",
|
||||
shippingLink: "Meh zu Versand & Zahlig →",
|
||||
thankYouEyebrow: "Merci", thankYouTitle: "Dini Bschtellig isch igange",
|
||||
thankYouLead: "Das isch momentan e Demo-Bstätigung (Phase 1, ohni echti Zahligsabwicklig). Sobald dr echt Checkout live isch, überchunnsch du da und per E-Mail e richtigi Bstellbstätigung.",
|
||||
continueShopping: "Wyter luege", total: "Total", free: "gratis",
|
||||
},
|
||||
account: {
|
||||
eyebrow: "Mis Konto", title: "Aamälde oder als Gast bstelle",
|
||||
todoNote: "Phase 1 – nume d'Oberflächi ohni echts Login/Registrierig. Bstellhistorie, Sändigsverfolgig, Wunschlischte und \"Zletscht aagluegt\" chöme i Phase 2.",
|
||||
loginTitle: "Aamälde", email: "E-Mail", password: "Passwort", loginButton: "Aamälde (chunnt i Phase 2)", forgotPassword: "Passwort vergässe?",
|
||||
newTitle: "Neu da?", newText: "Leg es Konto a, für Bstellhistorie, Wunschlischte und Sändigsverfolgig a einem Ort z'ha – oder bstell eifach als Gast, ganz ohni Konto.",
|
||||
guestButton: "Als Gast wyter yichaufe",
|
||||
},
|
||||
},
|
||||
fr: {
|
||||
nav: { shop: "Boutique", sale: "Soldes", creative: "À propos de la créatrice", faq: "FAQ", contact: "Contact", account: "Mon compte", login: "Se connecter" },
|
||||
topbar: "Livraison gratuite dès 75 € en DE/AT/CH/LU",
|
||||
footer: {
|
||||
tagline: "Fait main avec amour – de la créativité à offrir ou à garder.",
|
||||
shopHeading: "Boutique", serviceHeading: "Service", legalHeading: "Mentions légales",
|
||||
shipping: "Livraison & Paiement", account: "Mon compte",
|
||||
copyright: "Entreprise individuelle · Petite entreprise selon §19 UStG (droit allemand)",
|
||||
legalNoteTitle: "Remarque sur les pages légales",
|
||||
legalNote: "Les mentions légales, la politique de confidentialité, les CGV, le droit de rétractation et le formulaire type de rétractation ne sont disponibles qu'en allemand, pour des raisons de conformité juridique.",
|
||||
},
|
||||
badge: { neu: "Nouveau", bestseller: "Best-seller", sale: "Soldes", handgemacht: "Fait main", ausverkauft: "Épuisé" },
|
||||
common: {
|
||||
addToCart: "Ajouter au panier", soldOut: "Épuisé", addedToCart: "Ajouté au panier ✓",
|
||||
browseShop: "Découvrir la boutique", viewAll: "Tout voir →", viewAllSale: "Toutes les offres →",
|
||||
toShop: "Vers la boutique →", moreAboutMe: "En savoir plus sur moi →",
|
||||
vatNote: "Conformément à l'§19 UStG (droit allemand), aucune TVA n'est calculée ni indiquée.",
|
||||
allPricesPlusShipping: "Tous les prix hors frais de port.", plusShipping: "hors frais de port",
|
||||
quantity: "Quantité", photoSoon: "Photo du produit à venir", portraitSoon: "Photo de VanVan à venir",
|
||||
handmadeBadge: "🩵 Fait main",
|
||||
},
|
||||
product: {
|
||||
articleNo: "Référence", material: "Matière", size: "Taille", colors: "Couleurs",
|
||||
included: "Contenu de la livraison", care: "Entretien", delivery: "Délai de livraison",
|
||||
recommendations: "Cela pourrait aussi vous plaire",
|
||||
todoFields: "Les mentions GPSR, avertissements, coordonnées du fabricant et vidéos produit seront ajoutés dès que VanVan fournira les données réelles (phase 2, espace admin).",
|
||||
photoNote: "VanVan ajoutera de vraies photos plus tard",
|
||||
},
|
||||
shop: {
|
||||
title: "Tous les produits", lead: "Parcourez toutes les pièces faites main – filtrez et triez selon vos envies.",
|
||||
filters: "Filtres", allCategories: "Toutes les catégories", priceUpTo: "Prix jusqu'à", priceUpToValue: (v: number) => `jusqu'à ${v} €`,
|
||||
onlyAvailable: "Articles disponibles uniquement", filterNote: "Les filtres matière et couleur arriveront dès que l'assortiment sera plus large.",
|
||||
sortNew: "Nouveautés", sortBestseller: "Best-sellers", sortPriceAsc: "Prix croissant", sortPriceDesc: "Prix décroissant",
|
||||
resultCount: (n: number) => `${n} produit${n === 1 ? "" : "s"}`, noResults: "Aucun produit trouvé – veuillez ajuster les filtres.",
|
||||
breadcrumbShop: "Boutique", categoryEmpty: "Aucun produit dans cette catégorie pour le moment – repassez bientôt.",
|
||||
categoryLabel: "Catégorie", productSingular: "produit", productPlural: "produits", sortLabel: "Trier par", subcategoriesLabel: "Sous-catégories", subcategoryEmpty: "VanVan ajoutera bientôt les premiers produits ici – repassez bientôt.",
|
||||
},
|
||||
sale: {
|
||||
eyebrow: "% Soldes", title: "Offres actuelles", lead: "Réductions limitées dans le temps sur une sélection de pièces faites main.",
|
||||
empty: "Aucune promotion en cours actuellement – repassez bientôt.",
|
||||
},
|
||||
faq: {
|
||||
title: "Questions fréquentes",
|
||||
items: [
|
||||
{ q: "Combien de temps prend la livraison ?", a: "Généralement 2 à 7 jours ouvrés en Allemagne, Autriche, Suisse et au Luxembourg. Pour les pièces sur mesure, cela peut prendre un peu plus de temps – indiqué directement sur la page produit." },
|
||||
{ q: "À partir de quand la livraison est-elle gratuite ?", a: "À partir de 75 € d'achat, nous livrons gratuitement via DHL dans les pays desservis." },
|
||||
{ q: "Puis-je commander une pièce personnalisée ?", a: "Oui, avec plaisir ! Écrivez-nous simplement via le formulaire de contact – nous verrons ce qui est possible." },
|
||||
{ q: "Comment entretenir mes produits faits main ?", a: "Les instructions d'entretien figurent sur chaque page produit, un lavage à la main délicat est généralement recommandé." },
|
||||
{ q: "Quels moyens de paiement sont acceptés ?", a: "PayPal, Klarna et virement bancaire." },
|
||||
{ q: "Puis-je retourner ma commande ?", a: "Le droit de rétractation légal s'applique (voir notre notice de rétractation). Pour les pièces personnalisées sur mesure, le droit de rétractation peut être exclu par la loi." },
|
||||
{ q: "Dois-je payer la TVA ?", a: "Non. Van's DIY & Bastelbedarf est une petite entreprise selon l'§19 UStG – aucune TVA n'est calculée ni indiquée." },
|
||||
{ q: "Ai-je besoin d'un compte client pour commander ?", a: "Non, vous pouvez aussi commander confortablement en tant qu'invité. Un compte est toutefois utile pour l'historique des commandes et la liste de souhaits." },
|
||||
],
|
||||
},
|
||||
contact: {
|
||||
title: "Écrivez-nous", lead: "Des questions sur un produit, votre commande ou une demande personnalisée ? N'hésitez pas à nous contacter !",
|
||||
name: "Nom", email: "E-mail", subject: "Objet", message: "Message",
|
||||
gdprPrefix: "J'ai lu la", gdprLink: "politique de confidentialité", gdprSuffix: "et j'accepte le traitement de mes données.",
|
||||
send: "Envoyer le message", sentDemo: "Merci ! (Démo – pas encore d'envoi réel en phase 1)",
|
||||
todoNote: "À faire en phase 2 : connecter ce formulaire à un vrai service backend/e-mail (Cloudflare Worker). Actuellement aucun envoi réel n'a lieu.",
|
||||
directTitle: "Contact direct", emailPlaceholderNote: "(adresse provisoire)", orderNote: "Pour toute question de commande, merci d'indiquer le numéro de commande.",
|
||||
responseTitle: "Délai de réponse", responseNote: "Généralement sous 1 à 2 jours ouvrés.",
|
||||
},
|
||||
about: {
|
||||
eyebrow: "À propos de la créatrice", title: "Bonjour, je suis VanVan 🩵",
|
||||
lead: "Bienvenue dans mon petit atelier ! Tout ce que vous trouvez dans la boutique est fait main ici – point par point, perle par perle.",
|
||||
valuesTitle: "Ce qui compte pour moi",
|
||||
values: [
|
||||
{ icon: "🧵", title: "Artisanat", text: "Pas de production de masse – chaque pièce est réalisée individuellement et avec soin." },
|
||||
{ icon: "🩵", title: "Qualité", text: "Des matériaux choisis avec soin pour des résultats durables et beaux." },
|
||||
{ icon: "🎁", title: "Personnel", text: "Cadeau ou plaisir pour soi-même – chaque produit porte un peu de cœur." },
|
||||
],
|
||||
story: [
|
||||
{
|
||||
title: "Mon aventure a commencé avec une seule maille",
|
||||
paragraphs: [
|
||||
"Il y a environ six ans, j'ai découvert ma passion pour le crochet. Tout a commencé par pure curiosité. Sans cours ni expérience préalable, j'ai tout appris moi-même, étape par étape.",
|
||||
"À chaque nouvelle maille, chaque petite erreur et chaque projet terminé, non seulement mon savoir-faire grandissait, mais aussi mon enthousiasme pour cet artisanat merveilleux.",
|
||||
"Aujourd'hui, le crochet est pour moi bien plus qu'un simple loisir. C'est mon exutoire créatif, une façon de donner vie à mes idées et de faire sourire les gens avec des pièces uniques façonnées avec amour.",
|
||||
"La création d'amigurumis me fascine particulièrement. Je suis fascinée de voir comment une simple pelote de laine se transforme peu à peu en petit personnage — chacun avec son propre charme.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "La créativité ne connaît pas de limites",
|
||||
paragraphs: [
|
||||
"En plus du crochet, j'ai aussi découvert ma passion pour les bijoux faits main.",
|
||||
"Avec beaucoup de soin, je fabrique des bracelets, colliers, porte-clés et autres accessoires. J'adore combiner différents matériaux, couleurs et breloques pour créer sans cesse de nouveaux designs.",
|
||||
"Chaque bijou doit non seulement être beau, mais aussi apporter longtemps de la joie à son nouveau propriétaire.",
|
||||
"La créativité m'accompagne depuis toujours. C'est pourquoi vous trouverez chez moi non seulement du crochet classique ou des bijoux, mais toujours de nouvelles idées et projets créatifs, façonnés avec beaucoup d'amour.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "La qualité avec du cœur",
|
||||
paragraphs: [
|
||||
"Pour moi, l'artisanat, c'est la personnalité.",
|
||||
"Aucun produit ne sort d'une chaîne de production.",
|
||||
"Chaque pièce est soigneusement planifiée, façonnée et contrôlée par mes soins.",
|
||||
"J'accorde une importance particulière aux matériaux de qualité, à une finition soignée, à des détails pleins d'amour, à une qualité durable et à des designs uniques.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Bien plus qu'un simple loisir",
|
||||
paragraphs: [
|
||||
"Derrière ma créativité se cache bien plus que de la laine et des perles.",
|
||||
"Je suis quelqu'un qui met tout son cœur dans ce qu'elle fait. Quand je fais quelque chose, c'est avec passion, patience et l'envie de créer quelque chose de spécial.",
|
||||
"Cela me rend heureuse quand mes créations apportent de la joie aux autres — que ce soit comme cadeau pour un être cher ou comme petit plaisir pour soi-même.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Ma petite boutique de cœur",
|
||||
paragraphs: [
|
||||
"Ma boutique n'est donc pas simplement un endroit où l'on vend des produits.",
|
||||
"Elle fait partie de moi.",
|
||||
"D'innombrables heures de travail, de créativité, de passion et énormément d'amour se cachent dans chaque pièce.",
|
||||
"C'est exactement ce qui rend chaque pièce unique.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Merci d'être là",
|
||||
paragraphs: [
|
||||
"Merci beaucoup de prendre le temps de découvrir mon petit monde créatif et de soutenir ma passion.",
|
||||
"Je te souhaite beaucoup de plaisir à parcourir la boutique, et j'espère que tu trouveras exactement la pièce qui fera battre ton cœur plus fort.",
|
||||
],
|
||||
},
|
||||
],
|
||||
closing: "Fait main avec amour – maille après maille et perle après perle.",
|
||||
},
|
||||
shipping: {
|
||||
eyebrow: "Livraison & Paiement", title: "Livraison & Paiement",
|
||||
shippingTitle: "📦 Livraison",
|
||||
shippingItems: [
|
||||
"Livraison exclusivement par DHL", "Pays livrés : Allemagne, Autriche, Suisse, Luxembourg",
|
||||
"Délai de livraison : 2 à 7 jours ouvrés", "Les frais de port sont définis par produit et échelonnés selon la valeur de la commande et le pays",
|
||||
"Livraison gratuite dès 75 € d'achat", "L'expédition n'a lieu qu'après réception du paiement",
|
||||
],
|
||||
paymentTitle: "💳 Moyens de paiement", paymentItems: ["PayPal", "Klarna", "Virement bancaire (paiement anticipé)"],
|
||||
todoNote: "Le barème précis des frais de port par pays/valeur de commande sera ajouté dès que VanVan aura fixé les conditions DHL définitives.",
|
||||
},
|
||||
cart: {
|
||||
eyebrow: "Panier", title: "Votre panier", empty: "Votre panier est encore vide.",
|
||||
subtotal: "Sous-total", shipping: "Livraison", shippingCalculated: "calculée au moment du paiement",
|
||||
total: "Total", remaining: (v: string) => `Encore ${v} avant la livraison gratuite.`, freeShipping: "🩵 Vous bénéficiez de la livraison gratuite !",
|
||||
toCheckout: "Passer au paiement", remove: "Retirer", perItem: "/ pièce",
|
||||
},
|
||||
checkout: {
|
||||
eyebrow: "Paiement", title: "Finaliser la commande",
|
||||
todoNote: "Phase 1 – paiement de démonstration. Aucun paiement réel n'est encore traité. L'intégration complète avec PayPal, Klarna et le virement bancaire, ainsi que la gestion des commandes, suivront en phase 2.",
|
||||
step1: "1. Contact & adresse de livraison", email: "E-mail", firstName: "Prénom", lastName: "Nom",
|
||||
street: "Rue & numéro", zip: "Code postal", city: "Ville", country: "Pays",
|
||||
step2: "2. Moyen de paiement",
|
||||
step3: "3. Récapitulatif de commande", agbPrefix: "J'ai lu les", agbLink: "CGV", agbSuffix: "et je les accepte.",
|
||||
revocationPrefix: "J'ai pris connaissance de la", revocationLink: "notice de rétractation", revocationAnd: "et de la",
|
||||
privacyLink: "politique de confidentialité", revocationSuffix: ".",
|
||||
bankTransfer: "Virement bancaire (paiement anticipé)", orderButton: "Commander avec obligation de paiement", emptyCartAlert: "Votre panier est vide.",
|
||||
shippingHintTitle: "Remarque sur la livraison",
|
||||
shippingHint: "Livraison exclusivement par DHL en Allemagne, Autriche, Suisse et au Luxembourg. Délai de livraison 2 à 7 jours ouvrés. Livraison gratuite dès 75 €. L'expédition n'a lieu qu'après réception du paiement.",
|
||||
shippingLink: "En savoir plus sur la livraison & le paiement →",
|
||||
thankYouEyebrow: "Merci", thankYouTitle: "Votre commande a bien été reçue",
|
||||
thankYouLead: "Ceci est actuellement une confirmation de démonstration (phase 1, sans traitement réel du paiement). Une fois le vrai paiement en ligne, vous recevrez ici et par e-mail une véritable confirmation de commande.",
|
||||
continueShopping: "Continuer mes achats", total: "Total", free: "gratuit",
|
||||
},
|
||||
account: {
|
||||
eyebrow: "Mon compte", title: "Se connecter ou commander en tant qu'invité",
|
||||
todoNote: "Phase 1 – interface uniquement, pas encore de vraie connexion/inscription. L'historique des commandes, le suivi, la liste de souhaits et \"vu récemment\" suivront en phase 2.",
|
||||
loginTitle: "Se connecter", email: "E-mail", password: "Mot de passe", loginButton: "Se connecter (disponible en phase 2)", forgotPassword: "Mot de passe oublié ?",
|
||||
newTitle: "Nouveau ici ?", newText: "Créez un compte pour retrouver l'historique des commandes, la liste de souhaits et le suivi au même endroit – ou commandez simplement en tant qu'invité, sans compte.",
|
||||
guestButton: "Continuer mes achats en tant qu'invité",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type UiDict = (typeof ui)["de"];
|
||||
|
||||
export function useTranslations(locale: Locale): UiDict {
|
||||
return ui[locale];
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import { categories } from "../data/categories";
|
||||
import { defaultLocale, localeMeta, localePrefix, type Locale } from "../i18n/config";
|
||||
import { useTranslations } from "../i18n/ui";
|
||||
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
lang?: Locale;
|
||||
/** Sprachneutraler Pfad der aktuellen Seite (ohne Sprach-Prefix), z.B. "/shop/". Für den Sprachumschalter. */
|
||||
path?: string;
|
||||
/** Seite existiert nur auf Deutsch (Rechtstexte) — siehe LanguageSwitcher. */
|
||||
legalOnly?: boolean;
|
||||
}
|
||||
const {
|
||||
title,
|
||||
description = "Van's DIY & Bastelbedarf – handgemachte Häkelwerke, Plushies, Schmuck und Bastelzubehör mit Liebe gefertigt.",
|
||||
lang = defaultLocale,
|
||||
path = "/",
|
||||
legalOnly = false,
|
||||
} = Astro.props;
|
||||
|
||||
const t = useTranslations(lang);
|
||||
const p = (href: string) => `${localePrefix(lang)}${href}`;
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang={localeMeta[lang].htmlLang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title} · Van's DIY & Bastelbedarf</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={new URL(Astro.url.pathname, Astro.site ?? "https://vans-diy-bastelbedarf.de").toString()} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Van's DIY & Bastelbedarf" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta name="theme-color" content="#0a0910" />
|
||||
<!-- TODO Phase 2: Fonts self-hosten statt Google-Fonts-CDN (Performance/DSGVO) -->
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],300..700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root { --font-head-override: "Fraunces", serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">🩵 {t.topbar}</div>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a href={p("/")} class="brand">
|
||||
<img src="/logo.png" alt="Van's DIY & Bastelbedarf Logo" width="42" height="42" />
|
||||
<span>Van's DIY & Bastelbedarf</span>
|
||||
</a>
|
||||
|
||||
<nav class="main-nav" aria-label="Hauptnavigation">
|
||||
<a href={p("/shop/")}>{t.nav.shop}</a>
|
||||
<a href={p("/sale/")}>{t.nav.sale}</a>
|
||||
<a href={p("/ueber-die-kreative/")}>{t.nav.creative}</a>
|
||||
<a href={p("/faq/")}>{t.nav.faq}</a>
|
||||
<a href={p("/kontakt/")}>{t.nav.contact}</a>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<LanguageSwitcher lang={lang} path={path} legalOnly={legalOnly} />
|
||||
<a href={p("/konto/")} class="account-btn">
|
||||
<span class="account-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 12a5 5 0 1 0-5-5 5 5 0 0 0 5 5Zm0 2c-4.4 0-9 2.2-9 5v2h18v-2c0-2.8-4.6-5-9-5Z"/></svg>
|
||||
</span>
|
||||
<span class="account-label">{t.nav.login}</span>
|
||||
</a>
|
||||
<a href={p("/warenkorb/")} class="icon-link cart-link" aria-label={t.nav.account}>
|
||||
🧺 <span class="cart-count" id="cart-count">0</span>
|
||||
</a>
|
||||
<button class="hamburger" id="hamburger" aria-label="Menü" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="mobile-nav" id="mobile-nav" aria-label="Mobile Navigation">
|
||||
<a href={p("/shop/")}>{t.nav.shop}</a>
|
||||
<a href={p("/sale/")}>{t.nav.sale}</a>
|
||||
<a href={p("/ueber-die-kreative/")}>{t.nav.creative}</a>
|
||||
<a href={p("/faq/")}>{t.nav.faq}</a>
|
||||
<a href={p("/kontakt/")}>{t.nav.contact}</a>
|
||||
<a href={p("/konto/")}>{t.nav.account}</a>
|
||||
<span class="mobile-nav-divider">{t.shop.categoryLabel}</span>
|
||||
{categories.map((c) => <a href={p(`/shop/${c.slug}/`)}>{c.icon} {c.name[lang]}</a>)}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<nav class="sub-nav" aria-label="Kategorien-Schnellzugriff">
|
||||
<div class="container sub-nav-inner">
|
||||
{categories.map((c) => (
|
||||
<a href={p(`/shop/${c.slug}/`)}>{c.name[lang]}</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-grid">
|
||||
<div>
|
||||
<a href={p("/")} class="brand">
|
||||
<img src="/logo.png" alt="Van's DIY & Bastelbedarf Logo" width="36" height="36" />
|
||||
<span>Van's DIY & Bastelbedarf</span>
|
||||
</a>
|
||||
<p class="small">{t.footer.tagline}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t.footer.shopHeading}</h3>
|
||||
<ul class="footer-list">
|
||||
{categories.map((c) => <li><a href={p(`/shop/${c.slug}/`)}>{c.name[lang]}</a></li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t.footer.serviceHeading}</h3>
|
||||
<ul class="footer-list">
|
||||
<li><a href={p("/versand-zahlung/")}>{t.footer.shipping}</a></li>
|
||||
<li><a href={p("/faq/")}>{t.nav.faq}</a></li>
|
||||
<li><a href={p("/kontakt/")}>{t.nav.contact}</a></li>
|
||||
<li><a href={p("/konto/")}>{t.footer.account}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{t.footer.legalHeading}</h3>
|
||||
<ul class="footer-list">
|
||||
<li><a href="/impressum/">Impressum</a></li>
|
||||
<li><a href="/datenschutz/">Datenschutz</a></li>
|
||||
<li><a href="/agb/">AGB</a></li>
|
||||
<li><a href="/widerruf/">Widerrufsbelehrung</a></li>
|
||||
<li><a href="/muster-widerrufsformular/">Muster-Widerrufsformular</a></li>
|
||||
</ul>
|
||||
{lang !== "de" && <p class="small legal-note-footer" title={t.footer.legalNoteTitle}>{t.footer.legalNote}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<hr class="divider" />
|
||||
<p class="small">© {new Date().getFullYear()} Van's DIY & Bastelbedarf · {t.footer.copyright}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
import { cartCount } from "../scripts/cart";
|
||||
function updateCount() {
|
||||
const el = document.getElementById("cart-count");
|
||||
if (el) el.textContent = String(cartCount());
|
||||
}
|
||||
updateCount();
|
||||
window.addEventListener("cart:changed", updateCount);
|
||||
window.addEventListener("storage", updateCount);
|
||||
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const mobileNav = document.getElementById("mobile-nav");
|
||||
hamburger?.addEventListener("click", () => {
|
||||
const open = mobileNav?.classList.toggle("open");
|
||||
hamburger.setAttribute("aria-expanded", String(!!open));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
body { font-family: var(--font-body); }
|
||||
h1, h2, h3, h4 { font-family: var(--font-head-override, var(--font-head)); }
|
||||
|
||||
.topbar {
|
||||
background: linear-gradient(90deg, var(--c-purple), var(--c-purple-deep));
|
||||
color: var(--c-text);
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
padding: 0.5rem 1rem;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
background: rgba(10, 9, 16, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(244, 241, 247, 0.06);
|
||||
}
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-block: 0.8rem;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.brand img { border-radius: 50%; }
|
||||
|
||||
.main-nav { display: flex; gap: 1.6rem; }
|
||||
.main-nav a {
|
||||
font-size: 0.92rem;
|
||||
color: var(--c-text-muted);
|
||||
transition: color 0.2s var(--ease);
|
||||
}
|
||||
.main-nav a:hover { color: var(--c-accent); }
|
||||
|
||||
.header-actions { display: flex; align-items: center; gap: 0.8rem; }
|
||||
.icon-link { font-size: 1.1rem; position: relative; display: inline-flex; align-items: center; gap: 0.3rem; }
|
||||
.cart-count {
|
||||
background: var(--c-accent);
|
||||
color: #08181c;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
border-radius: 999px;
|
||||
min-width: 1.2em;
|
||||
padding: 0 0.35em;
|
||||
text-align: center;
|
||||
}
|
||||
.hamburger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--c-text);
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding: 0.5rem 1.5rem 1.2rem;
|
||||
gap: 0.9rem;
|
||||
border-top: 1px solid rgba(244,241,247,0.06);
|
||||
}
|
||||
.mobile-nav.open { display: flex; }
|
||||
.mobile-nav-divider {
|
||||
margin-top: 0.4rem;
|
||||
padding-top: 0.9rem;
|
||||
border-top: 1px solid rgba(244, 241, 247, 0.08);
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--c-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.main-nav { display: none; }
|
||||
.hamburger { display: inline-block; }
|
||||
}
|
||||
|
||||
/* Zweite Navigationsleiste: Schnellzugriff auf die 5 Hauptkategorien, direkt unter der
|
||||
Hauptnav — gleicher Designstil (dunkler Anthrazit-Streifen, Akzent-Unterstrich bei Hover). */
|
||||
.sub-nav {
|
||||
background: var(--c-anthracite);
|
||||
border-bottom: 1px solid rgba(244, 241, 247, 0.06);
|
||||
}
|
||||
.sub-nav-inner {
|
||||
display: flex;
|
||||
gap: clamp(1.1rem, 3vw, 2rem);
|
||||
overflow-x: auto;
|
||||
padding-block: 0.6rem;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.sub-nav-inner::-webkit-scrollbar { display: none; }
|
||||
.sub-nav-inner a {
|
||||
position: relative;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text-muted);
|
||||
white-space: nowrap;
|
||||
padding-bottom: 3px;
|
||||
transition: color 0.2s var(--ease);
|
||||
}
|
||||
.sub-nav-inner a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
height: 2px;
|
||||
background: var(--c-accent);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.2s var(--ease);
|
||||
}
|
||||
.sub-nav-inner a:hover { color: var(--c-accent); }
|
||||
.sub-nav-inner a:hover::after { transform: scaleX(1); }
|
||||
@media (max-width: 860px) { .sub-nav { display: none; } }
|
||||
|
||||
.site-footer {
|
||||
background: var(--c-anthracite);
|
||||
margin-top: 4rem;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.footer-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.55rem; }
|
||||
.footer-list a { color: var(--c-text-muted); font-size: 0.9rem; }
|
||||
.footer-list a:hover { color: var(--c-accent); }
|
||||
.legal-note-footer { margin-top: 0.8rem; font-size: 0.78rem; line-height: 1.5; }
|
||||
@media (max-width: 760px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 480px) { .footer-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,59 @@
|
||||
// Rein clientseitiger Warenkorb (Phase 1, kein Backend). Speichert im localStorage.
|
||||
// TODO Phase 2: durch echten Cloudflare-Worker-Warenkorb/Checkout ersetzen.
|
||||
|
||||
export interface CartItem {
|
||||
slug: string;
|
||||
name: string;
|
||||
preis: number;
|
||||
menge: number;
|
||||
}
|
||||
|
||||
const KEY = "vandiy_cart_v1";
|
||||
|
||||
export function getCart(): CartItem[] {
|
||||
if (typeof localStorage === "undefined") return [];
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(KEY) || "[]");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveCart(items: CartItem[]) {
|
||||
localStorage.setItem(KEY, JSON.stringify(items));
|
||||
window.dispatchEvent(new CustomEvent("cart:changed", { detail: items }));
|
||||
}
|
||||
|
||||
export function addToCart(item: Omit<CartItem, "menge">, menge = 1) {
|
||||
const cart = getCart();
|
||||
const existing = cart.find((i) => i.slug === item.slug);
|
||||
if (existing) {
|
||||
existing.menge += menge;
|
||||
} else {
|
||||
cart.push({ ...item, menge });
|
||||
}
|
||||
saveCart(cart);
|
||||
}
|
||||
|
||||
export function updateQuantity(slug: string, menge: number) {
|
||||
let cart = getCart();
|
||||
if (menge <= 0) {
|
||||
cart = cart.filter((i) => i.slug !== slug);
|
||||
} else {
|
||||
const item = cart.find((i) => i.slug === slug);
|
||||
if (item) item.menge = menge;
|
||||
}
|
||||
saveCart(cart);
|
||||
}
|
||||
|
||||
export function removeFromCart(slug: string) {
|
||||
saveCart(getCart().filter((i) => i.slug !== slug));
|
||||
}
|
||||
|
||||
export function cartCount(): number {
|
||||
return getCart().reduce((sum, i) => sum + i.menge, 0);
|
||||
}
|
||||
|
||||
export function cartTotal(): number {
|
||||
return getCart().reduce((sum, i) => sum + i.menge * i.preis, 0);
|
||||
}
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
Van's DIY & Bastelbedarf — Design-System
|
||||
Farbwerte 1:1 aus dem Vault: "Van Van – Homepage/02 Logo & Branding/Branding.md"
|
||||
Nicht ohne Rücksprache ändern — sind bereits WCAG-kontrastgeprüft.
|
||||
*/
|
||||
|
||||
/*
|
||||
Fonts: aktuell per Google-Fonts-<link> in Layout.astro eingebunden (siehe dort).
|
||||
TODO Phase 2: Fraunces + Inter self-hosten (woff2, DSGVO/Performance), sobald Zeit ist —
|
||||
Vorgehen wie bei bild.py/fonts_holen.sh im Bild-KI-Projekt (CSS2-API abfragen, .woff2-URLs
|
||||
aus der Antwort extrahieren, lokal unter public/fonts/ ablegen, @font-face umstellen).
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Farben */
|
||||
--c-black: #0a0910;
|
||||
--c-anthracite: #1b1a22;
|
||||
--c-anthracite-light: #252333;
|
||||
--c-purple: #2e1b47;
|
||||
--c-purple-deep: #1b1226;
|
||||
--c-text: #f4f1f7;
|
||||
--c-text-muted: #b7aec4;
|
||||
--c-accent: #5fd3ec;
|
||||
--c-accent-hover: #3ca9c2;
|
||||
--c-sale: #e7879a;
|
||||
--c-purple-glow: #9b7fd6; /* heller Violett-Ton NUR für Verlaufs-Ränder/Deko, nicht für Text */
|
||||
|
||||
/* Typografie */
|
||||
--font-head: "Fraunces", "Playfair Display", Georgia, serif;
|
||||
--font-body: "Inter", "Manrope", system-ui, sans-serif;
|
||||
|
||||
/* Form */
|
||||
--radius-s: 10px;
|
||||
--radius-m: 16px;
|
||||
--radius-l: 22px;
|
||||
--shadow-card: 0 12px 30px -12px rgba(46, 27, 71, 0.55);
|
||||
--shadow-card-hover: 0 18px 40px -12px rgba(95, 211, 236, 0.25);
|
||||
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--container: 1220px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--c-black);
|
||||
color: var(--c-text);
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
margin: 0 0 0.5em;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
h1 { font-size: clamp(2.2rem, 4vw + 1rem, 3.6rem); }
|
||||
h2 { font-size: clamp(1.7rem, 2vw + 1rem, 2.4rem); }
|
||||
h3 { font-size: 1.3rem; }
|
||||
p { margin: 0 0 1em; color: var(--c-text-muted); }
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
img { max-width: 100%; display: block; }
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--container);
|
||||
margin-inline: auto;
|
||||
padding-inline: 1.5rem;
|
||||
}
|
||||
|
||||
.section { padding: clamp(3rem, 6vw, 6rem) 0; }
|
||||
.section-tight { padding: clamp(2rem, 3vw, 3rem) 0; }
|
||||
|
||||
.eyebrow {
|
||||
display: inline-block;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-accent);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.lead { font-size: 1.15rem; color: var(--c-text); opacity: 0.9; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.small { font-size: 0.85rem; color: var(--c-text-muted); }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.85em 1.7em;
|
||||
border-radius: var(--radius-l);
|
||||
font-weight: 600;
|
||||
font-size: 0.98rem;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.25s var(--ease), background 0.25s var(--ease), box-shadow 0.25s var(--ease), border-color 0.25s var(--ease);
|
||||
}
|
||||
.btn:hover { transform: translateY(-2px); }
|
||||
.btn:active { transform: translateY(0); }
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--c-accent), var(--c-accent-hover));
|
||||
color: #08181c;
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
.btn-primary:hover { box-shadow: 0 20px 45px -14px rgba(95, 211, 236, 0.45); }
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
border-color: rgba(244, 241, 247, 0.25);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--c-accent); color: var(--c-accent); }
|
||||
|
||||
.btn-block { width: 100%; }
|
||||
.btn-row { display: flex; gap: 0.9rem; flex-wrap: wrap; margin-top: 1.5rem; }
|
||||
|
||||
/* Cards — Verlaufs-Rand statt trockenem Einzelfarb-Strich (Signature-Look der Seite) */
|
||||
.card {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: var(--radius-m);
|
||||
background:
|
||||
linear-gradient(165deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box,
|
||||
linear-gradient(135deg, rgba(95, 211, 236, 0.5), rgba(155, 127, 214, 0.4) 45%, rgba(231, 135, 154, 0.32)) border-box;
|
||||
padding: 1.6rem;
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease), filter 0.3s var(--ease);
|
||||
}
|
||||
.card::before {
|
||||
/* dezenter Glanz-Schimmer oben, wie ein Lichtreflex auf Glas — macht den Rand "speziell" statt flach */
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1.5px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.35), transparent 40%);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
|
||||
/* Klickbare Navigations-Kacheln (Produktkarten, Kategorien) bekommen den Verlaufs-Rand extra
|
||||
kräftig — das sind die "Kisten, wo man draufdrückt um auf eine neue Seite zu kommen". */
|
||||
a.card {
|
||||
background:
|
||||
linear-gradient(165deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box,
|
||||
linear-gradient(135deg, var(--c-accent), var(--c-purple-glow) 45%, var(--c-sale) 100%) border-box;
|
||||
}
|
||||
a.card:hover {
|
||||
box-shadow: var(--shadow-card-hover), 0 0 0 1px rgba(95, 211, 236, 0.25);
|
||||
}
|
||||
|
||||
/* Grid utils */
|
||||
.grid { display: grid; gap: 1.5rem; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
@media (max-width: 900px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 620px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.3em 0.75em;
|
||||
border-radius: 999px;
|
||||
background: rgba(95, 211, 236, 0.14);
|
||||
color: var(--c-accent);
|
||||
}
|
||||
.badge-sale { background: rgba(231, 135, 154, 0.16); color: var(--c-sale); }
|
||||
.badge-sold-out { background: rgba(183, 174, 196, 0.16); color: var(--c-text-muted); }
|
||||
|
||||
/* Placeholder art (kein echtes Produktfoto vorhanden) */
|
||||
.img-placeholder {
|
||||
position: relative;
|
||||
border-radius: var(--radius-m);
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, rgba(95, 211, 236, 0.18), transparent 55%),
|
||||
linear-gradient(155deg, var(--c-purple), var(--c-purple-deep));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
color: var(--c-text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
min-height: 160px;
|
||||
}
|
||||
.img-placeholder::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: repeating-linear-gradient(135deg, rgba(244,241,247,0.03) 0 2px, transparent 2px 14px);
|
||||
}
|
||||
|
||||
/* Portrait-Frame — edler Bilderrahmen für Porträtfotos (Startseite, Über die Kreative, ...) */
|
||||
.portrait-frame {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
.portrait-frame::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -6%;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, rgba(95, 211, 236, 0.35), transparent 60%),
|
||||
radial-gradient(circle at 75% 80%, rgba(231, 135, 154, 0.22), transparent 55%);
|
||||
filter: blur(38px);
|
||||
opacity: 0.85;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.portrait-frame .portrait-img,
|
||||
.portrait-frame img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-l);
|
||||
border: 1px solid rgba(244, 241, 247, 0.08);
|
||||
box-shadow: var(--shadow-card), 0 0 0 6px rgba(10, 9, 16, 0.6), 0 0 0 7px rgba(95, 211, 236, 0.18);
|
||||
transition: transform 0.5s var(--ease), box-shadow 0.5s var(--ease);
|
||||
}
|
||||
.portrait-frame:hover .portrait-img,
|
||||
.portrait-frame:hover img {
|
||||
transform: translateY(-4px) scale(1.015);
|
||||
box-shadow: var(--shadow-card-hover), 0 0 0 6px rgba(10, 9, 16, 0.6), 0 0 0 7px rgba(95, 211, 236, 0.32);
|
||||
}
|
||||
.portrait-badge {
|
||||
position: absolute;
|
||||
bottom: -1.1rem;
|
||||
right: -1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
padding: 0.7em 1.1em;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, var(--c-accent), var(--c-accent-hover));
|
||||
color: #08181c;
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 10px 26px -8px rgba(95, 211, 236, 0.55), 0 0 0 5px var(--c-black);
|
||||
transform: rotate(-4deg);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.portrait-badge { right: 0.4rem; bottom: -0.9rem; font-size: 0.78rem; padding: 0.6em 0.9em; }
|
||||
}
|
||||
|
||||
.todo-note {
|
||||
border: 1px dashed rgba(95, 211, 236, 0.4);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 0.9rem 1.1rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--c-text-muted);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.legal-note {
|
||||
border-left: 3px solid var(--c-accent);
|
||||
background: rgba(95, 211, 236, 0.08);
|
||||
padding: 1rem 1.3rem;
|
||||
border-radius: var(--radius-s);
|
||||
margin-bottom: 2rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
hr.divider {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(244, 241, 247, 0.08);
|
||||
margin: 2.5rem 0;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; }
|
||||
th, td { text-align: left; padding: 0.7rem 0.9rem; border-bottom: 1px solid rgba(244,241,247,0.08); }
|
||||
th { color: var(--c-text); font-family: var(--font-head); font-weight: 600; }
|
||||
|
||||
form.frm { display: grid; gap: 1rem; max-width: 560px; }
|
||||
.frm label { display: block; font-size: 0.85rem; color: var(--c-text-muted); margin-bottom: 0.35rem; }
|
||||
.frm input, .frm select, .frm textarea {
|
||||
width: 100%;
|
||||
padding: 0.75em 0.9em;
|
||||
border-radius: var(--radius-s);
|
||||
border: 1px solid rgba(244, 241, 247, 0.15);
|
||||
background: var(--c-anthracite);
|
||||
color: var(--c-text);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.frm input:focus, .frm select:focus, .frm textarea:focus,
|
||||
a:focus-visible, button:focus-visible {
|
||||
outline: 2px solid var(--c-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.frm .checkbox-row { display: flex; gap: 0.6rem; align-items: flex-start; font-size: 0.88rem; color: var(--c-text-muted); }
|
||||
.frm .checkbox-row input { width: auto; margin-top: 0.2rem; }
|
||||
|
||||
/* ============================================================
|
||||
Sprach-Umschalter + Konto-Button (Header)
|
||||
============================================================ */
|
||||
.lang-switch { position: relative; }
|
||||
.lang-switch-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4em;
|
||||
background:
|
||||
linear-gradient(155deg, color-mix(in srgb, var(--globe-color) 16%, var(--c-anthracite-light)), color-mix(in srgb, var(--globe-color) 5%, var(--c-anthracite)));
|
||||
border: 1px solid color-mix(in srgb, var(--globe-color) 42%, rgba(244, 241, 247, 0.14));
|
||||
color: var(--c-text);
|
||||
border-radius: 999px;
|
||||
padding: 0.4em 0.85em 0.4em 0.5em;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
inset 0 1px 0 color-mix(in srgb, var(--globe-glow, var(--globe-color)) 20%, transparent),
|
||||
0 3px 12px -6px color-mix(in srgb, var(--globe-color) 55%, transparent);
|
||||
transition: border-color 0.2s var(--ease), box-shadow 0.25s var(--ease), transform 0.2s var(--ease);
|
||||
}
|
||||
.lang-switch-btn:hover {
|
||||
border-color: color-mix(in srgb, var(--globe-color) 75%, transparent);
|
||||
box-shadow:
|
||||
inset 0 1px 0 color-mix(in srgb, var(--globe-glow, var(--globe-color)) 30%, transparent),
|
||||
0 6px 18px -6px color-mix(in srgb, var(--globe-color) 70%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.lang-switch-btn .chevron { font-size: 0.6rem; opacity: 0.7; color: var(--globe-color); transition: transform 0.2s var(--ease); }
|
||||
.lang-switch.open .lang-switch-btn .chevron { transform: rotate(180deg); }
|
||||
.lang-switch-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.6rem);
|
||||
right: 0;
|
||||
min-width: 168px;
|
||||
background: var(--c-anthracite-light);
|
||||
border: 1px solid rgba(244, 241, 247, 0.1);
|
||||
border-radius: var(--radius-m);
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 0.4rem;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
z-index: 60;
|
||||
}
|
||||
.lang-switch.open .lang-switch-menu { display: flex; }
|
||||
.lang-switch-menu a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
padding: 0.55em 0.7em;
|
||||
border-radius: var(--radius-s);
|
||||
font-size: 0.88rem;
|
||||
opacity: 0.72;
|
||||
transition: background 0.15s var(--ease), opacity 0.15s var(--ease);
|
||||
}
|
||||
.lang-switch-menu a:hover { background: color-mix(in srgb, var(--globe-color) 12%, transparent); opacity: 1; }
|
||||
.lang-switch-menu a[aria-current="true"] { opacity: 1; font-weight: 700; }
|
||||
.lang-switch-menu a[aria-current="true"] .lang-globe {
|
||||
box-shadow:
|
||||
0 0 0 3px color-mix(in srgb, var(--globe-color) 45%, transparent),
|
||||
0 0 14px 2px color-mix(in srgb, var(--globe-glow, var(--globe-color)) 70%, transparent);
|
||||
}
|
||||
|
||||
/* Sprachname im Farbverlauf der Nationalflagge (2 kräftigste Streifenfarben, Weiß/Schwarz
|
||||
ausgelassen weil auf dunklem Grund unsichtbar bzw. zu hart) — Globus und Schrift wirken
|
||||
dadurch wie ein zusammengehöriges, flaggenfarbenes Element. */
|
||||
.lang-label {
|
||||
background: linear-gradient(90deg, var(--globe-color, var(--c-text)), var(--globe-glow, var(--globe-color)));
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 10px color-mix(in srgb, var(--globe-glow, var(--globe-color)) 35%, transparent);
|
||||
transition: filter 0.2s var(--ease);
|
||||
}
|
||||
.lang-switch-btn:hover .lang-label,
|
||||
.lang-switch-menu a:hover .lang-label {
|
||||
filter: brightness(1.25) saturate(1.2);
|
||||
}
|
||||
|
||||
/* Globus-Icon statt Flaggen-Emoji: trägt die echten Nationalflaggen-Farben als rotierende
|
||||
Farbscheibe (siehe `flag` in i18n/config.ts) — dreht sich dauerhaft langsam wie ein
|
||||
Modellglobus, wird schneller beim Hover. Eine zweite, FESTE Ebene obendrüber sorgt für
|
||||
Glanzlicht + Schattenseite (3D-Kugel-Wirkung), damit die Farben plastisch statt flach wirken. */
|
||||
.lang-globe {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
color: rgba(10, 9, 16, 0.55);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--globe-color, var(--c-accent)) 55%, transparent),
|
||||
0 3px 5px -1px color-mix(in srgb, black 55%, transparent),
|
||||
0 0 9px 1px color-mix(in srgb, var(--globe-glow, var(--globe-color)) 55%, transparent);
|
||||
transition: box-shadow 0.25s var(--ease), transform 0.25s var(--ease);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lang-globe::after {
|
||||
/* fixe Lichtquelle: bleibt stehen, während die Flaggenfarben darunter rotieren */
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
background:
|
||||
radial-gradient(circle at 28% 22%, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0) 30%),
|
||||
radial-gradient(circle at 75% 78%, rgba(0, 0, 0, 0.65) 0%, rgba(0, 0, 0, 0) 55%);
|
||||
mix-blend-mode: overlay;
|
||||
pointer-events: none;
|
||||
}
|
||||
.lang-globe-rotor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: conic-gradient(from 0deg, var(--flag-1) 0deg 120deg, var(--flag-2) 120deg 240deg, var(--flag-3) 240deg 360deg);
|
||||
animation: globe-spin 6s linear infinite;
|
||||
}
|
||||
.lang-globe:hover .lang-globe-rotor { animation-duration: 1.6s; }
|
||||
@keyframes globe-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lang-globe-rotor { animation: none; }
|
||||
}
|
||||
.lang-globe svg { position: relative; z-index: 1; opacity: 0.55; filter: drop-shadow(0 0 1px rgba(10, 9, 16, 0.6)); }
|
||||
.lang-switch-btn .lang-globe { width: 21px; height: 21px; }
|
||||
.lang-switch-btn:hover .lang-globe,
|
||||
.lang-switch-menu a:hover .lang-globe {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--globe-color, var(--c-accent)) 70%, transparent),
|
||||
0 3px 6px -1px color-mix(in srgb, black 55%, transparent),
|
||||
0 0 16px 3px color-mix(in srgb, var(--globe-glow, var(--globe-color)) 80%, transparent);
|
||||
transform: scale(1.12);
|
||||
}
|
||||
|
||||
.account-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
padding: 0.5em 1em 0.5em 0.55em;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, rgba(95, 211, 236, 0.16), rgba(46, 27, 71, 0.4));
|
||||
border: 1px solid rgba(95, 211, 236, 0.3);
|
||||
color: var(--c-text);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.2s var(--ease), transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
|
||||
}
|
||||
.account-btn:hover {
|
||||
border-color: var(--c-accent);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 20px -8px rgba(95, 211, 236, 0.4);
|
||||
}
|
||||
.account-btn .account-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(155deg, var(--c-accent), var(--c-purple));
|
||||
color: var(--c-black);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.account-btn .account-label { display: none; }
|
||||
.account-btn { padding: 0.5em; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Geteilte Seiten-Layouts (von allen Sprachversionen genutzt,
|
||||
damit DE/EN/CH/FR immer exakt gleich aussehen)
|
||||
============================================================ */
|
||||
|
||||
/* Startseite: Hero + Kategorie-Kacheln + Kreative-Sektion */
|
||||
.hero { position: relative; overflow: hidden; padding: clamp(3.5rem, 8vw, 7rem) 0; background: radial-gradient(circle at 15% 20%, rgba(46,27,71,0.9), var(--c-black) 60%); }
|
||||
.hero-watermark {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(85vw, 900px);
|
||||
opacity: 0.045;
|
||||
filter: grayscale(1);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.hero-inner { position: relative; max-width: 640px; }
|
||||
.hero h1 { margin-bottom: 0.7em; }
|
||||
|
||||
/* Zweispaltiges Hero-Layout: Foto-Collage links, Text rechts */
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.95fr) 1.1fr;
|
||||
gap: clamp(2rem, 5vw, 4.5rem);
|
||||
align-items: center;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.hero-content { grid-template-columns: 1fr; }
|
||||
.hero-inner { max-width: none; }
|
||||
}
|
||||
|
||||
/* Foto-Collage: 4 handgefertigte Produktfotos, versetzt & leicht gedreht arrangiert wie ein
|
||||
kuratiertes Moodboard statt eines steifen Rasters — jedes Foto trägt denselben
|
||||
Verlaufs-Rand wie unsere Produktkarten (Signature-Look, siehe a.card), damit es sich
|
||||
erkennbar in die restliche Seite einfügt statt wie ein Fremdkörper zu wirken. */
|
||||
.hero-gallery {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
aspect-ratio: 1 / 1.08;
|
||||
margin-inline: auto;
|
||||
}
|
||||
.hero-photo {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-l);
|
||||
border: 2.5px solid transparent;
|
||||
background:
|
||||
linear-gradient(165deg, var(--c-anthracite-light), var(--c-anthracite)) padding-box,
|
||||
linear-gradient(135deg, var(--c-accent), var(--c-purple-glow) 45%, var(--c-sale) 100%) border-box;
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: transform 0.4s var(--ease), box-shadow 0.4s var(--ease), z-index 0s;
|
||||
}
|
||||
.hero-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.hero-photo:hover { transform: scale(1.06) rotate(0deg) !important; box-shadow: var(--shadow-card-hover); z-index: 5 !important; }
|
||||
|
||||
.hero-photo-1 { width: 58%; aspect-ratio: 3 / 4; top: 0; left: 0; transform: rotate(-4deg); z-index: 3; }
|
||||
.hero-photo-2 { width: 44%; aspect-ratio: 1 / 1; top: 3%; right: 0; transform: rotate(5deg); z-index: 2; }
|
||||
.hero-photo-3 { width: 40%; aspect-ratio: 4 / 5; bottom: 4%; left: 12%; transform: rotate(6deg); z-index: 4; }
|
||||
.hero-photo-4 { width: 38%; aspect-ratio: 1 / 1; bottom: -1%; right: 8%; transform: rotate(-6deg); z-index: 1; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-gallery { max-width: 380px; margin-bottom: 1rem; }
|
||||
}
|
||||
@media (max-width: 760px) { .hero-watermark { opacity: 0.035; } }
|
||||
|
||||
.category-card { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 0.4rem; }
|
||||
.cat-icon { font-size: 2.1rem; }
|
||||
.category-card h3 { margin-bottom: 0.2rem; }
|
||||
|
||||
.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 1.3rem; }
|
||||
.section-head a { color: var(--c-accent); }
|
||||
|
||||
.creator-section { align-items: center; }
|
||||
.creator-photo { max-width: 420px; margin-inline: auto; }
|
||||
|
||||
/* "Über die Kreative" */
|
||||
.intro { align-items: center; }
|
||||
/* Breiter als vorher (war 440px) + Seitenverhältnis nah am Originalfoto (900x1353 ≈ 2/3)
|
||||
statt 3/4 — vorher wurde oben/unten so stark beschnitten, dass das Plushie neben VanVan
|
||||
(Oktopus) nur noch als Ecke sichtbar war. So ist das ganze Foto inkl. beider Plushies zu sehen. */
|
||||
.portrait { max-width: 540px; }
|
||||
.portrait .portrait-img,
|
||||
.portrait img { aspect-ratio: 2 / 3; }
|
||||
|
||||
/* Pull-Quote: elegantes, hervorgehobenes Zitat — genutzt für VanVans Zitat auf der Startseite
|
||||
("Die Kreative") und den Abschluss-Satz auf "Über die Kreative". Serif-Kursiv + Akzent-Rand
|
||||
statt einfachem Fließtext, damit es sich sichtbar abhebt, aber im Stil der Seite bleibt. */
|
||||
.pull-quote {
|
||||
position: relative;
|
||||
font-family: var(--font-head);
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-size: clamp(1.2rem, 1.1rem + 1vw, 1.55rem);
|
||||
line-height: 1.55;
|
||||
color: var(--c-text);
|
||||
padding: 0.2rem 0 0.2rem 1.4rem;
|
||||
border-left: 3px solid var(--c-accent);
|
||||
margin: 1.4rem 0;
|
||||
}
|
||||
.pull-quote-center {
|
||||
text-align: center;
|
||||
border-left: none;
|
||||
border-top: 1px solid rgba(95, 211, 236, 0.3);
|
||||
border-bottom: 1px solid rgba(95, 211, 236, 0.3);
|
||||
padding: 1.6rem 1rem;
|
||||
max-width: 720px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Story-Abschnitte ("Über die Kreative"): wiederkehrende h2+Absätze mit klarer Struktur und
|
||||
sanftem Einblenden beim Scrollen (dezente Verbesserung, kein Redesign). */
|
||||
.story-section { padding-block: 1.6rem; }
|
||||
.story-section:not(:last-child) { border-bottom: 1px solid rgba(244, 241, 247, 0.07); }
|
||||
.story-section h2 { color: var(--c-text); }
|
||||
.story-section p { max-width: 68ch; }
|
||||
.story-section.is-visible { animation: story-fade-up 0.7s var(--ease) both; }
|
||||
@keyframes story-fade-up {
|
||||
from { opacity: 0; transform: translateY(18px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.story-section.is-visible { animation: none; }
|
||||
}
|
||||
|
||||
/* Unterkategorie-Chips auf den Hauptkategorie-Seiten */
|
||||
.subcategory-chips { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 1.5rem 0 2.5rem; }
|
||||
.subcategory-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.55em 1.1em;
|
||||
border-radius: 999px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-text-muted);
|
||||
background: var(--c-anthracite);
|
||||
border: 1px solid rgba(244, 241, 247, 0.12);
|
||||
transition: color 0.2s var(--ease), border-color 0.2s var(--ease), background 0.2s var(--ease);
|
||||
}
|
||||
.subcategory-chip:hover { color: var(--c-accent); border-color: color-mix(in srgb, var(--c-accent) 55%, transparent); background: color-mix(in srgb, var(--c-accent) 8%, var(--c-anthracite)); }
|
||||
|
||||
/* Seiten-Banner (Shop, Sale, ...): echtes Foto/Grafik als Kopfbild mit Verlauf-Überblendung
|
||||
unten, damit Titel/Text darauf lesbar bleiben — ersetzt die reine Text-Kopfzeile durch ein
|
||||
echtes visuelles Signal, wie im Pflichtenheft für "Bannerplätze" gefordert. */
|
||||
.page-banner {
|
||||
position: relative;
|
||||
/* aspect-ratio statt fester min-height, damit das Bild bei background-size:cover NICHT
|
||||
oben/unten beschnitten wird — die Box hat exakt das Seitenverhältnis des Fotos, jedes
|
||||
Banner setzt seinen eigenen Wert per Inline-Style (--banner-ratio). */
|
||||
aspect-ratio: var(--banner-ratio, 1600 / 729);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: var(--radius-l);
|
||||
overflow: hidden;
|
||||
margin-inline: auto;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.page-banner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(10, 9, 16, 0.15) 0%, rgba(10, 9, 16, 0.55) 55%, rgba(10, 9, 16, 0.92) 100%),
|
||||
linear-gradient(100deg, rgba(46, 27, 71, 0.35), transparent 55%);
|
||||
}
|
||||
.page-banner-content { position: relative; z-index: 1; padding: 1.8rem clamp(1.25rem, 4vw, 2.5rem); }
|
||||
.page-banner-content .eyebrow { color: var(--c-accent); }
|
||||
.page-banner-content h1 { margin-bottom: 0.35em; text-shadow: 0 2px 12px rgba(0,0,0,0.5); }
|
||||
.page-banner-content .lead { color: var(--c-text); max-width: 620px; }
|
||||
|
||||
/* Shop-Übersicht + Kategorie-Seiten */
|
||||
.shop-layout { display: grid; grid-template-columns: 240px 1fr; gap: 2rem; align-items: start; }
|
||||
.filters h3 { margin-bottom: 1rem; }
|
||||
.filter-group { margin-bottom: 1.2rem; }
|
||||
.filter-group label { display: block; font-size: 0.85rem; color: var(--c-text-muted); margin-bottom: 0.4rem; }
|
||||
.filter-group select, .filter-group input[type="range"] { width: 100%; }
|
||||
.filters select, .shop-toolbar select {
|
||||
background: var(--c-anthracite); color: var(--c-text);
|
||||
border: 1px solid rgba(244,241,247,0.15); border-radius: var(--radius-s); padding: 0.5em 0.7em;
|
||||
}
|
||||
.shop-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; }
|
||||
.legal-hint { margin-top: 2rem; }
|
||||
.breadcrumb { margin-bottom: 0.6rem; }
|
||||
.breadcrumb a { color: var(--c-accent); }
|
||||
@media (max-width: 800px) { .shop-layout { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Produktseite */
|
||||
.product-detail { align-items: start; }
|
||||
.product-hero { aspect-ratio: 1/1; }
|
||||
.thumb-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.7rem; margin-top: 0.7rem; }
|
||||
.thumb { aspect-ratio: 1/1; min-height: 0; }
|
||||
.badge-row { display: flex; gap: 0.5rem; margin-bottom: 0.8rem; flex-wrap: wrap; }
|
||||
.price-row-detail { display: flex; align-items: baseline; gap: 0.8rem; margin-bottom: 0.2rem; }
|
||||
.price-detail { font-size: 1.8rem; font-weight: 700; color: var(--c-accent); }
|
||||
.qty-row { display: flex; align-items: center; gap: 0.8rem; margin: 1.4rem 0 0.5rem; }
|
||||
.qty-row input { width: 4.5rem; padding: 0.6em; border-radius: var(--radius-s); border: 1px solid rgba(244,241,247,0.15); background: var(--c-anthracite); color: var(--c-text); }
|
||||
.specs { margin-top: 1.5rem; }
|
||||
.specs th { width: 40%; color: var(--c-text-muted); font-weight: 500; font-family: var(--font-body); }
|
||||
|
||||
/* FAQ */
|
||||
.faq-list { display: grid; gap: 1rem; max-width: 760px; }
|
||||
.faq-item summary { cursor: pointer; font-family: var(--font-head); font-weight: 600; color: var(--c-text); list-style: none; display: block; }
|
||||
.faq-item summary::-webkit-details-marker { display: none; }
|
||||
.faq-item summary::before { content: "+ "; color: var(--c-accent); }
|
||||
.faq-item[open] summary::before { content: "- "; }
|
||||
.faq-item p { margin-top: 0.8rem; margin-bottom: 0; }
|
||||
|
||||
/* Kontakt */
|
||||
.contact-layout { align-items: start; }
|
||||
|
||||
/* Warenkorb */
|
||||
.cart-layout { display: grid; grid-template-columns: 1fr 320px; gap: 2rem; align-items: start; }
|
||||
.cart-item { display: flex; align-items: center; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid rgba(244,241,247,0.08); }
|
||||
.cart-item:last-child { border-bottom: none; }
|
||||
.cart-item .thumb { width: 64px; height: 64px; flex-shrink: 0; }
|
||||
.cart-item .info { flex: 1; }
|
||||
.cart-item .qty-controls { display: flex; align-items: center; gap: 0.4rem; }
|
||||
.cart-item .qty-controls button { width: 28px; height: 28px; border-radius: 8px; border: 1px solid rgba(244,241,247,0.15); background: var(--c-anthracite); color: var(--c-text); cursor: pointer; }
|
||||
.cart-item .remove { color: var(--c-sale); font-size: 0.8rem; margin-left: 0.8rem; }
|
||||
.summary-row { display: flex; justify-content: space-between; margin: 0.5rem 0; font-size: 0.92rem; color: var(--c-text-muted); }
|
||||
.summary-row.total { color: var(--c-text); font-weight: 700; font-size: 1.15rem; }
|
||||
@media (max-width: 800px) { .cart-layout { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Checkout */
|
||||
.checkout-layout { display: grid; grid-template-columns: 1fr 300px; gap: 2rem; align-items: start; }
|
||||
.payment-options { display: grid; gap: 0.6rem; }
|
||||
.pay-option { display: flex; align-items: center; gap: 0.6rem; padding: 0.7em 1em; border: 1px solid rgba(244,241,247,0.12); border-radius: var(--radius-s); font-size: 0.92rem; }
|
||||
.checkout-summary .row { display: flex; justify-content: space-between; font-size: 0.9rem; padding: 0.3rem 0; color: var(--c-text-muted); }
|
||||
.checkout-summary .row.total { color: var(--c-text); font-weight: 700; }
|
||||
@media (max-width: 800px) { .checkout-layout { grid-template-columns: 1fr; } }
|
||||
.danke { max-width: 560px; margin-inline: auto; }
|
||||
|
||||
/* Konto */
|
||||
.account-grid { align-items: start; }
|
||||
Reference in New Issue
Block a user