first commit

This commit is contained in:
torbinus
2026-07-06 13:37:08 +02:00
commit 7e51514383
171 changed files with 29200 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Bestellung erfolgreich"
description="Vielen Dank für Ihre Bestellung bei place4bees."
>
<section class="mx-auto max-w-2xl px-4 sm:px-6 lg:px-8 py-16 text-center">
<h1 class="text-3xl font-semibold text-brand-ink">Vielen Dank für Ihre Bestellung!</h1>
<p id="order-summary" class="mt-4 text-brand-text">Bestellbestätigung wird geladen…</p>
<a href="/shop" class="mt-8 inline-block text-brand-primary underline">Weiter einkaufen</a>
</section>
</Layout>
<script>
const raw = sessionStorage.getItem("last_order");
const el = document.getElementById("order-summary");
if (raw && el) {
const order = JSON.parse(raw);
el.textContent = `Bestellnummer ${order.display_id ?? order.id}. Eine Bestätigung geht an ${order.email}.`;
sessionStorage.removeItem("last_order");
} else if (el) {
el.textContent = "Keine aktuelle Bestellung gefunden.";
}
</script>
+25
View File
@@ -0,0 +1,25 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Anfrage"
description="Interesse an eigenen Bienenvölkern für Ihr Unternehmen? Jetzt unverbindlich anfragen."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-3xl font-semibold text-brand-ink">Interesse?</h1>
<p class="mt-4 text-brand-text">
Schreiben Sie uns, wir melden uns mit einem individuellen Angebot für Ihren Standort.
</p>
<form class="mt-8 grid gap-4 max-w-md">
<input type="text" name="company" placeholder="Unternehmen" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="text" name="name" placeholder="Ansprechpartner" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="email" name="email" placeholder="E-Mail" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="text" name="location" placeholder="Standort (Stadt)" class="rounded-lg border border-brand-border px-4 py-2" />
<textarea name="message" placeholder="Ihre Nachricht" rows="4" class="rounded-lg border border-brand-border px-4 py-2"></textarea>
<button type="submit" class="rounded-full bg-brand-primary text-brand-ink px-6 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Anfrage senden
</button>
</form>
</section>
</Layout>
+18
View File
@@ -0,0 +1,18 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Unser Angebot"
description="Bienenvölker, professionelle Betreuung und Team-Erlebnisse für Unternehmen und Hotels."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16 prose prose-stone">
<h1 class="text-3xl font-semibold text-brand-ink">Unser Angebot</h1>
<p class="mt-4 text-brand-text">
Wir bringen eigene Bienenvölker an Ihren Firmen- oder Hotelstandort und übernehmen die
komplette fachliche Betreuung. Ihr Unternehmen profitiert von einem nachhaltigen Image,
eigenem Honig als Geschenk oder Give-away, und einem greifbaren Beitrag zum Artenschutz
inklusive Erlebnisangeboten für Ihr Team vor Ort.
</p>
</section>
</Layout>
@@ -0,0 +1,63 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Ihr Projekt"
description="So funktioniert ein Unternehmensbienen-Projekt mit place4bees von der Beratung bis zur Honigernte."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Eigene Bienen vor Ort. Professionell betreut. Nachhaltig erlebbar.</p>
<h1 class="mt-2 text-4xl text-brand-ink">Ihr Projekt</h1>
<h2 class="mt-10 text-2xl text-brand-ink">So funktioniert's</h2>
<p class="mt-4 text-brand-text normal-case">
place4bees ermöglicht es Unternehmen, ohne imkerliche Erfahrung Bienenvölker auf dem
Firmengelände anzusiedeln. Wir kümmern uns um die intensive Betreuung das ganze Jahr über,
regeln alle Formalitäten und gesetzlichen Vorgaben und unterstützen bei der medialen
Aufbereitung.
</p>
<h2 class="mt-10 text-2xl text-brand-ink">Ablauf vor Projektstart</h2>
<ol class="mt-4 space-y-2 text-brand-text normal-case list-decimal list-inside">
<li>Kennenlernen &amp; Beratungsgespräch</li>
<li>Standortauswahl mit lokalem Imker vor Ort</li>
<li>Individuelles Angebot</li>
<li>Vorbereitung der Bienenvölker</li>
<li>Die Bienen ziehen ein!</li>
</ol>
<h2 class="mt-10 text-2xl text-brand-ink">Highlights</h2>
<div class="mt-4 grid gap-6 sm:grid-cols-3">
<div>
<p class="font-semibold text-brand-ink normal-case">Honigernte</p>
<p class="mt-1 text-sm text-brand-text normal-case">12 Mal jährlich eigenen Honig aus Ihrem direkten Umfeld, auf Wunsch mit individuellem Design.</p>
</div>
<div>
<p class="font-semibold text-brand-ink normal-case">PR &amp; Media</p>
<p class="mt-1 text-sm text-brand-text normal-case">Wir unterstützen bei der medialen Aufbereitung Ihres nachhaltigen Engagements.</p>
</div>
<div>
<p class="font-semibold text-brand-ink normal-case">Events</p>
<p class="mt-1 text-sm text-brand-text normal-case">Regelmäßige Besuche der Imker:innen vor Ort sowie Workshops, Vorträge und Kurse.</p>
</div>
</div>
<h2 class="mt-10 text-2xl text-brand-ink">Leistungen umfassen</h2>
<ul class="mt-4 grid gap-2 sm:grid-cols-2 text-brand-text normal-case list-disc list-inside">
<li>Bienenstand einrichten</li>
<li>Honigernte und -verarbeitung</li>
<li>Event-Organisation</li>
<li>Übernahme aller Formalitäten</li>
<li>Individueller Unternehmenshonig</li>
<li>Ersatz für Verluste (versichert gegen Diebstahl und Vandalismus)</li>
<li>Ganzjährige Betreuung</li>
<li>PR &amp; Social Media Support</li>
<li>Rückholung der Bienen bei Projektende</li>
</ul>
<a href="/business/anfrage" class="mt-10 inline-block rounded bg-brand-primary text-brand-ink px-7 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Interesse? Jetzt anfragen
</a>
</section>
</Layout>
+11
View File
@@ -0,0 +1,11 @@
---
import Layout from "../../layouts/Layout.astro";
import BusinessHome from "../../components/sections/BusinessHome.astro";
---
<Layout
title="Bienenvölker für Unternehmen"
description="place4bees betreut Bienenvölker direkt an Ihrem Firmen- oder Hotelstandort professionell, nachhaltig und erlebbar."
>
<BusinessHome />
</Layout>
+36
View File
@@ -0,0 +1,36 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Kontakt"
description="Holen Sie sich zusammen mit place4bees ein Stück Natur in Ihr Unternehmen."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Partnerschaftliche vertrauensvolle Zusammenarbeit</p>
<h1 class="mt-2 text-4xl text-brand-ink">Holen Sie sich zusammen mit uns ein Stück Natur in Ihr Unternehmen</h1>
<p class="mt-4 text-brand-text normal-case">
place4bees ist in Hamburg zuhause und arbeitet deutschlandweit und in der DACH-Region mit
festen Vertragsimkern zusammen.
</p>
<form class="mt-8 grid gap-4 max-w-md">
<input type="text" name="name" placeholder="Name" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="email" name="email" placeholder="E-Mail" required class="rounded-lg border border-brand-border px-4 py-2" />
<textarea name="message" placeholder="Ihr Anliegen" rows="4" required class="rounded-lg border border-brand-border px-4 py-2"></textarea>
<label class="flex items-start gap-2 text-sm text-brand-text-muted normal-case">
<input type="checkbox" required class="mt-1" />
Ich stimme der Datenschutzerklärung zu.
</label>
<button type="submit" class="rounded bg-brand-primary text-brand-ink px-6 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Senden
</button>
</form>
<div class="mt-10 text-sm text-brand-text-muted normal-case">
<p>place4bees GmbH</p>
<p>Isestr. 109, 20149 Hamburg</p>
<p>kontakt@place4bees.com · +49 162-2 35 02 02</p>
</div>
</section>
</Layout>
@@ -0,0 +1,64 @@
---
import Layout from "../../layouts/Layout.astro";
---
<Layout
title="Philosophie"
description="Verantwortung. Regionalität. Wertschätzung. Die Philosophie hinter place4bees."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Verantwortung. Regionalität. Wertschätzung.</p>
<h1 class="mt-2 text-4xl text-brand-ink">Was uns antreibt</h1>
<h2 class="mt-10 text-2xl text-brand-ink">Unsere Vision</h2>
<p class="mt-4 text-brand-text normal-case">
Als Hamburger Stadtimker möchten wir nicht nur möglichst viele artgerechte Lebensräume für
die Honigbienen etablieren, sondern folgende Ziele verfolgen:
</p>
<ul class="mt-4 space-y-2 text-brand-text normal-case list-disc list-inside">
<li>Aufmerksamkeit und Wertschätzung für eines der wichtigsten Nutztiere weltweit schaffen</li>
<li>Unternehmen und Hotels eine besondere Möglichkeit bieten, sich im lokalen Umweltschutz zu engagieren</li>
<li>Vielen Menschen Einblicke in die Welt der Bienen ermöglichen</li>
<li>Durch die Honigbiene ein Bewusstsein für die Gefährdung von Wildbienenarten schaffen</li>
</ul>
<div class="mt-10 rounded-2xl bg-white ring-1 ring-brand-border overflow-hidden grid sm:grid-cols-2 items-stretch">
<img src="/images/beekeepers-quote.jpg" alt="Imker:innen von place4bees bei der Arbeit" class="w-full h-64 sm:h-full object-cover" />
<div class="p-6 sm:p-8 flex flex-col justify-center">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Was uns antreibt</p>
<p class="mt-3 text-lg text-brand-ink normal-case italic">
„Wir haben es uns zur Aufgabe gemacht, mithilfe von zukunftsorientierten Unternehmen und
Hotels den heimischen Honigbienen artgerechte Lebensräume zu schaffen und sie so stark
für die Zukunft zu machen."
</p>
</div>
</div>
<h2 class="mt-10 text-2xl text-brand-ink">Starke Partner</h2>
<p class="mt-4 text-brand-text normal-case">
Wir setzen auf vertrauensvolle Partnerschaft und arbeiten nachhaltig mit regionalem Fokus.
Mit über 20 Jahren Erfahrung in Unternehmenskommunikation unterstützen wir unsere Kunden
dabei, gesunde, kräftige Bienenvölker aufzubauen.
</p>
<h2 class="mt-10 text-2xl text-brand-ink">Unsere Werte</h2>
<div class="mt-4 grid gap-6 sm:grid-cols-2">
<div>
<p class="font-semibold text-brand-ink normal-case">Die Tiere</p>
<p class="mt-1 text-sm text-brand-text normal-case">Tierwohl hat oberste Priorität.</p>
</div>
<div>
<p class="font-semibold text-brand-ink normal-case">Die Imkerei</p>
<p class="mt-1 text-sm text-brand-text normal-case">Moderne Ansätze mit Respekt vor konventioneller Imkerei.</p>
</div>
<div>
<p class="font-semibold text-brand-ink normal-case">Unser Netzwerk</p>
<p class="mt-1 text-sm text-brand-text normal-case">Austausch mit Bienenexperten.</p>
</div>
<div>
<p class="font-semibold text-brand-ink normal-case">Jedes Projekt</p>
<p class="mt-1 text-sm text-brand-text normal-case">Maßgeschneiderte Konzepte für individuelle Bedürfnisse.</p>
</div>
</div>
</section>
</Layout>
+40
View File
@@ -0,0 +1,40 @@
---
import Layout from "../../layouts/Layout.astro";
const artikel = [
"noordish 2023",
"green LIFESTYLE 2023",
"Brigitte 2023",
"Couch 2023",
"Szene Hamburg Essen+Trinken 2023/2024",
"Hamburger Morgenpost 2025",
];
---
<Layout
title="Presse"
description="Was man über place4bees schreibt und hört."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Verantwortung. Regionalität. Wertschätzung.</p>
<h1 class="mt-2 text-4xl text-brand-ink">Presse</h1>
<h2 class="mt-10 text-2xl text-brand-ink">Was man über uns schreibt</h2>
<ul class="mt-4 space-y-2 text-brand-text normal-case list-disc list-inside">
{artikel.map((a) => <li>{a}</li>)}
</ul>
<h2 class="mt-10 text-2xl text-brand-ink">Hier kann man was über uns hören</h2>
<p class="mt-4 text-brand-text normal-case">„Der Finanzgourmet Dani Zastrow: Die Bienenkönigin" (Podcast)</p>
<h2 class="mt-10 text-2xl text-brand-ink">Was wir über uns schreiben</h2>
<ul class="mt-4 space-y-2 text-brand-text normal-case list-disc list-inside">
<li>Unsere Brand Story</li>
<li>Unsere Gründerstory</li>
</ul>
<p class="mt-10 text-sm text-brand-text-muted normal-case">
place4bees GmbH · Isestr. 109 · 20149 Hamburg · kontakt@place4bees.com · +49 162-2 35 02 02
</p>
</section>
</Layout>
@@ -0,0 +1,44 @@
---
import Layout from "../../layouts/Layout.astro";
const team = [
{ name: "Dani Zastrow", role: "Co-Founder, Head of B2B" },
{ name: "Björn Schumann", role: "Co-Founder, Head of Operations" },
{ name: "Niklas Bartelmann", role: "Technischer Leiter & ausgebildeter Imker" },
{ name: "Meike Fischer", role: "Backoffice & Marketing" },
{ name: "Nico Göllnitz", role: "Assistenz Imkerei & Hamburger Standorte" },
{ name: "Doro Scriba", role: "Verantwortliche Imkerin Düsseldorf" },
{ name: "Sabine Schließer", role: "Imkerei Support & Allround Power" },
{ name: "Klaus Bamberger", role: "Verantwortlicher Imker Detmold" },
{ name: "Rudi", role: "Büro Wachdienst & Feel Good Manager" },
];
---
<Layout
title="Über uns"
description="Das Team hinter place4bees moderne, gesunde Imkerei für Unternehmen und Hotels."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Eigene Bienen vor Ort. Professionell betreut. Nachhaltig erlebbar.</p>
<h1 class="mt-2 text-4xl text-brand-ink">Über uns</h1>
<p class="mt-6 text-brand-text normal-case">
place4bees wurde gegründet, um moderne und gesunde Imkerei in urbanen und ländlichen
Regionen zu fördern. Unser Team verfügt über umfangreiche Imkerei-Expertise sowie 24 Jahre
Erfahrung in Banking, Marketing, Kommunikation und Nachhaltigkeitsmanagement bei großen
Hamburger Unternehmen. Unsere gemeinsame Leidenschaft für Bienen motiviert uns, die Imkerei
strukturell zu stärken. place4bees ist in Hamburg ansässig und arbeitet deutschlandweit
sowie in der DACH-Region mit vertragsgebundenen Imkern zusammen.
</p>
<h2 class="mt-10 text-2xl text-brand-ink">Unser Team</h2>
<div class="mt-6 grid gap-6 sm:grid-cols-2">
{team.map((member) => (
<div class="rounded-lg bg-white ring-1 ring-brand-border p-4">
<p class="font-semibold text-brand-ink normal-case">{member.name}</p>
<p class="text-sm text-brand-text-muted normal-case">{member.role}</p>
</div>
))}
</div>
</section>
</Layout>
+22
View File
@@ -0,0 +1,22 @@
---
export const prerender = false;
import Layout from "../layouts/Layout.astro";
import PrivateHome from "../components/sections/PrivateHome.astro";
import BusinessHome from "../components/sections/BusinessHome.astro";
import { isBusinessHost } from "../lib/sections";
const hostname = Astro.request.headers.get("host") ?? Astro.url.hostname;
const showBusiness = isBusinessHost(hostname);
---
<Layout
title={showBusiness ? "Bienenvölker für Unternehmen" : "Regionaler Honig aus Hamburg"}
description={
showBusiness
? "place4bees betreut Bienenvölker direkt an Ihrem Firmen- oder Hotelstandort professionell, nachhaltig und erlebbar."
: "place4bees: regionaler Honig, Bienenprodukte und Bienenpatenschaften aus Hamburg auch mitten in der Stadt."
}
>
{showBusiness ? <BusinessHome /> : <PrivateHome />}
</Layout>
+129
View File
@@ -0,0 +1,129 @@
---
import Layout from "../layouts/Layout.astro";
import { getCart, listShippingOptions } from "../lib/cart";
export const prerender = false;
const cartId = Astro.cookies.get("cart_id")?.value;
let cart = null;
let shippingOptions: Awaited<ReturnType<typeof listShippingOptions>> = [];
if (cartId) {
try {
cart = await getCart(cartId);
shippingOptions = await listShippingOptions(cartId);
} catch {
cart = null;
}
}
const items = cart?.items ?? [];
---
<Layout title="Kasse" description="Bestellung bei place4bees abschließen.">
<section class="mx-auto max-w-2xl px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<h1 class="text-3xl font-semibold text-brand-ink">Zur Kasse</h1>
{items.length === 0 && (
<p class="mt-6 text-brand-text">
Ihr Warenkorb ist leer. <a href="/shop" class="text-brand-primary underline">Zum Shop</a>
</p>
)}
{items.length > 0 && (
<form id="checkout-form" class="mt-8 grid gap-4">
<input type="email" name="email" placeholder="E-Mail" required class="rounded-lg border border-brand-border px-4 py-2" />
<div class="grid grid-cols-2 gap-4">
<input type="text" name="first_name" placeholder="Vorname" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="text" name="last_name" placeholder="Nachname" required class="rounded-lg border border-brand-border px-4 py-2" />
</div>
<input type="text" name="address_1" placeholder="Straße und Hausnummer" required class="rounded-lg border border-brand-border px-4 py-2" />
<div class="grid grid-cols-2 gap-4">
<input type="text" name="postal_code" placeholder="PLZ" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="text" name="city" placeholder="Stadt" required class="rounded-lg border border-brand-border px-4 py-2" />
</div>
<input type="hidden" name="country_code" value="de" />
<fieldset class="mt-4">
<legend class="font-medium text-brand-ink mb-2">Versand</legend>
<div class="grid gap-2">
{shippingOptions.map((option, index) => (
<label class="flex items-center gap-2 text-sm text-brand-text">
<input type="radio" name="shipping_option_id" value={option.id} required checked={index === 0} />
{option.name} {option.amount.toFixed(2)} €
</label>
))}
</div>
</fieldset>
<p id="checkout-status" class="text-sm text-red-600 hidden"></p>
<button
type="submit"
class="mt-4 rounded-full bg-brand-primary text-brand-ink px-6 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors disabled:opacity-50"
>
Zahlungspflichtig bestellen
</button>
</form>
)}
</section>
</Layout>
<script>
import { getCartIdClient, clearCartIdClient } from "../lib/cartCookie";
import { updateCart, addShippingMethod, createPaymentCollection, createPaymentSession, completeCart } from "../lib/cart";
const form = document.getElementById("checkout-form") as HTMLFormElement | null;
const status = document.getElementById("checkout-status");
form?.addEventListener("submit", async (event) => {
event.preventDefault();
const cartId = getCartIdClient();
if (!cartId) return;
const submitButton = form.querySelector("button[type=submit]") as HTMLButtonElement;
submitButton.disabled = true;
status?.classList.add("hidden");
try {
const data = new FormData(form);
await updateCart(cartId, {
email: String(data.get("email")),
shipping_address: {
first_name: String(data.get("first_name")),
last_name: String(data.get("last_name")),
address_1: String(data.get("address_1")),
postal_code: String(data.get("postal_code")),
city: String(data.get("city")),
country_code: String(data.get("country_code")),
},
});
await addShippingMethod(cartId, String(data.get("shipping_option_id")));
const paymentCollection = await createPaymentCollection(cartId);
await createPaymentSession(paymentCollection.id, "pp_system_default");
const result = await completeCart(cartId);
if (result.type === "order") {
sessionStorage.setItem("last_order", JSON.stringify(result.order));
clearCartIdClient();
window.location.href = "/bestellung/danke";
} else {
throw new Error(result.error?.message || "Bestellung konnte nicht abgeschlossen werden.");
}
} catch (err) {
if (status) {
status.textContent = err instanceof Error ? err.message : "Es ist ein Fehler aufgetreten.";
status.classList.remove("hidden");
}
submitButton.disabled = false;
}
});
</script>
+21
View File
@@ -0,0 +1,21 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout
title="Kontakt"
description="Kontaktieren Sie place4bees in Hamburg."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16">
<h1 class="text-3xl font-semibold text-brand-ink">Kontakt</h1>
<p class="mt-4 text-brand-text">Fragen zu unserem Honig oder den Patenschaften? Schreiben Sie uns.</p>
<form class="mt-8 grid gap-4 max-w-md">
<input type="text" name="name" placeholder="Name" required class="rounded-lg border border-brand-border px-4 py-2" />
<input type="email" name="email" placeholder="E-Mail" required class="rounded-lg border border-brand-border px-4 py-2" />
<textarea name="message" placeholder="Ihre Nachricht" rows="4" required class="rounded-lg border border-brand-border px-4 py-2"></textarea>
<button type="submit" class="rounded-full bg-brand-primary text-brand-ink px-6 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Absenden
</button>
</form>
</section>
</Layout>
+11
View File
@@ -0,0 +1,11 @@
---
import Layout from "../layouts/Layout.astro";
import PrivateHome from "../components/sections/PrivateHome.astro";
---
<Layout
title="Regionaler Honig aus Hamburg"
description="place4bees: regionaler Honig, Bienenprodukte und Bienenpatenschaften aus Hamburg auch mitten in der Stadt."
>
<PrivateHome />
</Layout>
+146
View File
@@ -0,0 +1,146 @@
---
import Layout from "../../layouts/Layout.astro";
import AddToCartButton from "../../components/AddToCartButton.astro";
import { getProductByHandle, getDefaultRegion } from "../../lib/medusa";
import { formatEUR, formatGrundpreis } from "../../lib/format";
export const prerender = false;
const { handle } = Astro.params;
const region = await getDefaultRegion();
const product = handle ? await getProductByHandle(handle, region.id) : null;
if (!product) {
return Astro.redirect("/shop");
}
const infoByVariant = Object.fromEntries(
product.variants.map((variant) => [
variant.id,
{
price: variant.calculated_price ? formatEUR(variant.calculated_price.calculated_amount) : "",
grundpreis:
variant.calculated_price && variant.metadata?.net_weight_grams
? formatGrundpreis(variant.calculated_price.calculated_amount, variant.metadata.net_weight_grams)
: null,
},
])
);
const initial = infoByVariant[product.variants[0]?.id] ?? { price: "", grundpreis: null };
const deliveryTime = product.metadata?.delivery_time;
const gallery = (product.images?.length ? product.images.map((i) => i.url) : product.thumbnail ? [product.thumbnail] : []);
---
<Layout title={product.title} description={product.description ?? product.title}>
<section class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
<nav class="flex items-center gap-2 text-sm text-brand-text-muted">
<a href="/shop" class="hover:text-brand-primary transition-colors">Shop</a>
<span>/</span>
{product.metadata?.category && (
<>
<span>{product.metadata.category}</span>
<span>/</span>
</>
)}
<span class="text-brand-text">{product.title}</span>
</nav>
<div class="mt-8 grid gap-12 sm:grid-cols-2">
<div>
<div id="main-image" class="aspect-square rounded-2xl bg-brand-surface overflow-hidden">
{gallery[0] && <img src={gallery[0]} alt={product.title} class="h-full w-full object-cover" />}
</div>
{gallery.length > 1 && (
<div class="mt-4 flex gap-3">
{gallery.map((url, index) => (
<button
class:list={[
"gallery-thumb h-16 w-16 rounded-lg overflow-hidden ring-2 transition-colors",
index === 0 ? "ring-brand-primary" : "ring-transparent",
]}
data-src={url}
>
<img src={url} alt="" class="h-full w-full object-cover" />
</button>
))}
</div>
)}
</div>
<div>
{product.metadata?.category && (
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">{product.metadata.category}</p>
)}
<h1 class="mt-2 text-3xl font-semibold text-brand-ink">{product.title}</h1>
<div class="mt-3 flex items-baseline gap-3">
<p id="product-price" class="text-2xl font-semibold text-brand-primary">{initial.price}</p>
<span class="text-sm text-brand-text-muted">inkl. MwSt.</span>
</div>
<p id="product-grundpreis" class="mt-1 text-sm text-brand-text-muted">{initial.grundpreis ?? ""}</p>
<p class="mt-6 text-brand-text whitespace-pre-line">{product.description}</p>
<div class="mt-8">
<AddToCartButton variants={product.variants} regionId={region.id} />
</div>
<div class="mt-8 grid gap-3 sm:grid-cols-2">
{deliveryTime && (
<p class="flex items-center gap-2 text-sm text-brand-text-muted">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Lieferzeit: {deliveryTime}
</p>
)}
<p class="flex items-center gap-2 text-sm text-brand-text-muted">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Regional aus Hamburg
</p>
<p class="flex items-center gap-2 text-sm text-brand-text-muted">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Nachhaltig produziert
</p>
</div>
</div>
</div>
</section>
<script type="application/json" id="variant-info" set:html={JSON.stringify(infoByVariant)} />
</Layout>
<script>
const priceEl = document.getElementById("product-price");
const grundpreisEl = document.getElementById("product-grundpreis");
const infoJson = document.getElementById("variant-info")?.textContent;
if (priceEl && grundpreisEl && infoJson) {
const info: Record<string, { price: string; grundpreis: string | null }> = JSON.parse(infoJson);
document.querySelectorAll<HTMLInputElement>('input[name="variant"]').forEach((input) => {
input.addEventListener("change", () => {
const entry = info[input.value];
priceEl.textContent = entry?.price ?? "";
grundpreisEl.textContent = entry?.grundpreis ?? "";
});
});
}
const mainImage = document.querySelector<HTMLImageElement>("#main-image img");
document.querySelectorAll<HTMLButtonElement>(".gallery-thumb").forEach((thumb) => {
thumb.addEventListener("click", () => {
if (mainImage && thumb.dataset.src) mainImage.src = thumb.dataset.src;
document.querySelectorAll(".gallery-thumb").forEach((t) => {
t.classList.remove("ring-brand-primary");
t.classList.add("ring-transparent");
});
thumb.classList.remove("ring-transparent");
thumb.classList.add("ring-brand-primary");
});
});
</script>
+133
View File
@@ -0,0 +1,133 @@
---
export const prerender = false;
import Layout from "../../layouts/Layout.astro";
import { listProducts, getDefaultRegion } from "../../lib/medusa";
import { formatEUR, formatGrundpreis } from "../../lib/format";
let products: Awaited<ReturnType<typeof listProducts>> = [];
let loadError = false;
try {
const region = await getDefaultRegion();
products = await listProducts(region.id);
} catch {
loadError = true;
}
function priceLabel(product: (typeof products)[number]) {
const prices = product.variants
.map((v) => v.calculated_price?.calculated_amount)
.filter((amount): amount is number => amount != null);
if (prices.length === 0) return null;
const min = Math.min(...prices);
return prices.length > 1 && new Set(prices).size > 1 ? `ab ${formatEUR(min)}` : formatEUR(min);
}
function grundpreisLabel(product: (typeof products)[number]) {
const variant = product.variants.find((v) => v.metadata?.net_weight_grams);
if (!variant?.calculated_price || !variant.metadata?.net_weight_grams) return null;
return formatGrundpreis(variant.calculated_price.calculated_amount, variant.metadata.net_weight_grams);
}
const categories = [...new Set(products.map((p) => p.metadata?.category).filter(Boolean))] as string[];
---
<Layout
title="Shop"
description="Regionaler Honig, Bienenwachsprodukte und Geschenkideen von place4bees direkt aus Hamburg bestellen."
>
<section class="bg-brand-surface">
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-16 sm:py-20">
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-brand-accent">Regional · Nachhaltig · Hamburg</p>
<h1 class="mt-3 text-4xl sm:text-5xl text-brand-ink">Der Shop</h1>
<p class="mt-4 text-brand-text max-w-xl normal-case">
Regionaler Honig, Bienenprodukte und Geschenkideen direkt aus Hamburg. Alle Preise inkl. MwSt.
</p>
</div>
</section>
<section class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
{loadError && (
<p class="rounded-lg bg-red-50 text-red-700 px-4 py-3 text-sm">
Die Produkte konnten aktuell nicht geladen werden. Bitte später erneut versuchen.
</p>
)}
{categories.length > 1 && (
<div id="category-filters" class="flex flex-wrap gap-2">
<button data-category="" class="category-pill rounded-full px-4 py-1.5 text-sm font-medium transition-colors bg-brand-ink text-brand-cream">
Alle
</button>
{categories.map((category) => (
<button
data-category={category}
class="category-pill rounded-full px-4 py-1.5 text-sm font-medium transition-colors bg-brand-surface text-brand-text hover:bg-brand-surface-strong"
>
{category}
</button>
))}
</div>
)}
<div id="product-grid" class="mt-10 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => {
const price = priceLabel(product);
const grundpreis = grundpreisLabel(product);
return (
<a
href={`/shop/${product.handle}`}
data-category={product.metadata?.category ?? ""}
class="product-card group block rounded-2xl bg-white shadow-sm ring-1 ring-brand-border overflow-hidden transition-all hover:-translate-y-1 hover:shadow-xl"
>
<div class="aspect-square bg-brand-surface relative overflow-hidden">
{product.metadata?.category && (
<span class="absolute top-3 left-3 rounded-full bg-brand-ink/80 text-brand-cream text-xs font-medium px-3 py-1 backdrop-blur-sm">
{product.metadata.category}
</span>
)}
{product.thumbnail && (
<img src={product.thumbnail} alt={product.title} class="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300" />
)}
</div>
<div class="p-5">
<h2 class="font-semibold text-brand-ink leading-snug">{product.title}</h2>
<div class="mt-2 flex items-baseline justify-between gap-2">
{price && <span class="font-semibold text-brand-primary">{price}</span>}
{grundpreis && <span class="text-xs text-brand-text-muted">{grundpreis}</span>}
</div>
</div>
</a>
);
})}
</div>
{!loadError && products.length === 0 && (
<p class="mt-10 text-brand-text-muted">Aktuell sind keine Produkte verfügbar.</p>
)}
</section>
</Layout>
<script>
const pills = document.querySelectorAll<HTMLButtonElement>(".category-pill");
const cards = document.querySelectorAll<HTMLAnchorElement>(".product-card");
pills.forEach((pill) => {
pill.addEventListener("click", () => {
const category = pill.dataset.category ?? "";
pills.forEach((p) => {
const active = p === pill;
p.classList.toggle("bg-brand-ink", active);
p.classList.toggle("text-brand-cream", active);
p.classList.toggle("bg-brand-surface", !active);
p.classList.toggle("text-brand-text", !active);
});
cards.forEach((card) => {
const matches = !category || card.dataset.category === category;
card.classList.toggle("hidden", !matches);
});
});
});
</script>
+21
View File
@@ -0,0 +1,21 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout
title="Über uns"
description="place4bees betreut Bienenvölker in und um Hamburg auch mitten in der Stadt."
>
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-16 prose prose-stone">
<h1 class="text-3xl font-semibold text-brand-ink">Über uns</h1>
<p class="mt-4 text-brand-text">
place4bees produziert regionalen Honig in und um Hamburg einige unserer Völker stehen
mitten in der Stadt. Neben Honig in verschiedenen Sorten bieten wir Bienenwachsprodukte,
Schnupperkurse und Bienenpatenschaften an.
</p>
<p class="mt-4 text-brand-text">
Unseren Honig und weitere regionale Produkte gibt es außerdem an mehreren
24/7-Verkaufsautomaten in Hamburg.
</p>
</section>
</Layout>
+95
View File
@@ -0,0 +1,95 @@
---
import Layout from "../layouts/Layout.astro";
import { getCart } from "../lib/cart";
export const prerender = false;
const cartId = Astro.cookies.get("cart_id")?.value;
let cart = null;
if (cartId) {
try {
cart = await getCart(cartId);
} catch {
cart = null;
}
}
const items = cart?.items ?? [];
---
<Layout title="Warenkorb" description="Ihr Warenkorb bei place4bees.">
<section class="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<h1 class="text-3xl font-semibold text-brand-ink">Warenkorb</h1>
{items.length === 0 && (
<p class="mt-6 text-brand-text">
Ihr Warenkorb ist leer. <a href="/shop" class="text-brand-primary underline">Zum Shop</a>
</p>
)}
{items.length > 0 && (
<>
<ul class="mt-8 divide-y divide-brand-border">
{items.map((item) => (
<li class="py-4 flex items-center gap-4" data-line-item-id={item.id}>
<div class="h-16 w-16 rounded-lg bg-brand-surface overflow-hidden shrink-0">
{item.thumbnail && <img src={item.thumbnail} alt={item.title} class="h-full w-full object-cover" />}
</div>
<div class="flex-1">
<p class="font-medium text-brand-ink">{item.title}</p>
<p class="text-sm text-brand-text-muted">{item.unit_price.toFixed(2)} € / Stück</p>
</div>
<input
type="number"
min="1"
value={item.quantity}
class="line-item-qty w-16 rounded-lg border border-brand-border px-2 py-1"
/>
<button class="remove-line-item text-sm text-brand-text-muted hover:text-red-600">Entfernen</button>
</li>
))}
</ul>
<div class="mt-8 flex items-center justify-between">
<p class="text-lg font-semibold text-brand-ink">Summe: {(cart?.item_total ?? 0).toFixed(2)} €</p>
<a
href="/kasse"
class="rounded-full bg-brand-primary text-brand-ink px-6 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors"
>
Zur Kasse
</a>
</div>
</>
)}
</section>
</Layout>
<script>
import { getCartIdClient } from "../lib/cartCookie";
import { updateLineItem, removeLineItem } from "../lib/cart";
const cartId = getCartIdClient();
document.querySelectorAll<HTMLInputElement>(".line-item-qty").forEach((input) => {
input.addEventListener("change", async () => {
const li = input.closest<HTMLElement>("[data-line-item-id]");
const lineId = li?.dataset.lineItemId;
if (!cartId || !lineId) return;
await updateLineItem(cartId, lineId, Number(input.value));
window.dispatchEvent(new CustomEvent("cart:updated"));
location.reload();
});
});
document.querySelectorAll<HTMLButtonElement>(".remove-line-item").forEach((button) => {
button.addEventListener("click", async () => {
const li = button.closest<HTMLElement>("[data-line-item-id]");
const lineId = li?.dataset.lineItemId;
if (!cartId || !lineId) return;
await removeLineItem(cartId, lineId);
window.dispatchEvent(new CustomEvent("cart:updated"));
location.reload();
});
});
</script>