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
+24
View File
@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
## Development
When starting the dev server, use background mode:
```
astro dev --background
```
Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`.
## Documentation
Full documentation: https://docs.astro.build
Consult these guides before working on related tasks:
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+43
View File
@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
+22
View File
@@ -0,0 +1,22 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
vite: {
plugins: [tailwindcss()],
server: {
// Lets us test host-based behavior (hhonig.de vs. place4bees.com) locally
// by sending a custom Host header against the dev server.
allowedHosts: ['hhonig.de', 'place4bees.com']
}
},
adapter: node({
mode: 'standalone'
})
});
+4703
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "frontend",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^11.0.1",
"@fontsource-variable/open-sans": "^5.2.7",
"@fontsource/bebas-neue": "^5.2.7",
"@tailwindcss/vite": "^4.3.2",
"astro": "^7.0.5",
"tailwindcss": "^4.3.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

@@ -0,0 +1,85 @@
---
interface Variant {
id: string;
title: string;
}
interface Props {
variants: Variant[];
regionId: string;
}
const { variants, regionId } = Astro.props;
---
<div>
{variants.length > 1 && (
<div id="variant-picker" class="mb-5 flex flex-wrap gap-2">
{variants.map((variant, index) => (
<label class="relative">
<input
type="radio"
name="variant"
value={variant.id}
checked={index === 0}
class="peer sr-only"
/>
<span class="block cursor-pointer rounded-full border border-brand-border px-4 py-2 text-sm font-medium text-brand-text transition-colors peer-checked:border-brand-primary peer-checked:bg-brand-primary peer-checked:text-white hover:border-brand-primary">
{variant.title}
</span>
</label>
))}
</div>
)}
<button
id="add-to-cart"
data-variant-id={variants[0]?.id}
data-region-id={regionId}
class="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"
>
In den Warenkorb
</button>
<p id="add-to-cart-status" class="mt-3 text-sm text-brand-text-muted hidden"></p>
</div>
<script>
import { createCart, addLineItem } from "../lib/cart";
import { getCartIdClient, setCartIdClient } from "../lib/cartCookie";
const button = document.getElementById("add-to-cart") as HTMLButtonElement | null;
const status = document.getElementById("add-to-cart-status");
function selectedVariantId(): string | undefined {
const checked = document.querySelector<HTMLInputElement>('input[name="variant"]:checked');
return checked?.value || button?.dataset.variantId;
}
button?.addEventListener("click", async () => {
const variantId = selectedVariantId();
if (!variantId) return;
button.disabled = true;
try {
let cartId = getCartIdClient();
if (!cartId) {
const cart = await createCart(button.dataset.regionId!);
cartId = cart.id;
setCartIdClient(cartId);
}
await addLineItem(cartId, variantId, 1);
window.dispatchEvent(new CustomEvent("cart:updated"));
if (status) {
status.textContent = "Zum Warenkorb hinzugefügt.";
status.classList.remove("hidden");
}
} catch {
if (status) {
status.textContent = "Das hat leider nicht geklappt. Bitte erneut versuchen.";
status.classList.remove("hidden");
}
} finally {
button.disabled = false;
}
});
</script>
+28
View File
@@ -0,0 +1,28 @@
---
import { sections, COMPANY_NAME, type Section } from "../lib/sections";
interface Props {
section: Section;
}
const { section } = Astro.props;
const config = sections[section];
const other = section === "private" ? sections.business : sections.private;
const year = new Date().getFullYear();
---
<footer class="mt-24 bg-white border-t border-brand-border">
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-14 grid gap-8 sm:grid-cols-3">
<div>
<p class="font-display text-2xl text-brand-ink">{COMPANY_NAME}</p>
<p class="text-sm text-brand-text-muted mt-1 normal-case">{config.tagline}</p>
</div>
<div class="text-sm text-brand-text-muted">
<p class="font-medium text-brand-ink mb-2 normal-case">Auch interessant</p>
<a href={other.homeHref} class="hover:text-brand-primary transition-colors">{other.label}</a>
</div>
<div class="text-sm text-brand-text-muted">
<p class="normal-case">&copy; {year} {COMPANY_NAME}</p>
</div>
</div>
</footer>
+163
View File
@@ -0,0 +1,163 @@
---
import { sections, COMPANY_NAME, type Section } from "../lib/sections";
interface Props {
section: Section;
}
const { section } = Astro.props;
const config = sections[section];
const currentPath = Astro.url.pathname;
---
<header class="border-b border-brand-ink/10 bg-white/80 backdrop-blur sticky top-0 z-50">
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between gap-4">
<a href={config.homeHref} class="flex items-center gap-2 shrink-0">
<img src="/images/place4bees-logo.png" alt="" class="h-10 w-10" />
<span class="font-display text-xl text-brand-ink">{COMPANY_NAME}</span>
</a>
<div class="hidden md:flex items-center gap-1 rounded-full bg-brand-surface p-1">
{Object.entries(sections).map(([key, s]) => (
<a
href={s.homeHref}
class:list={[
"rounded-full px-4 py-1.5 text-sm font-semibold transition-colors",
key === section ? "bg-brand-primary text-brand-ink" : "text-brand-text hover:text-brand-primary",
]}
>
{s.label}
</a>
))}
</div>
<nav class="hidden md:flex gap-6 items-center">
{config.nav.map((item) => (
<a
href={item.href}
class:list={[
"text-sm font-medium hover:text-brand-primary transition-colors",
currentPath === item.href ? "text-brand-primary" : "text-brand-text",
]}
>
{item.label}
</a>
))}
{section === "private" && (
<a href="/warenkorb" class="relative inline-flex items-center hover:opacity-80 transition-opacity" aria-label="Warenkorb">
<span class="sr-only">Warenkorb</span>
<svg viewBox="0 0 24 24" class="h-7 w-7" aria-hidden="true">
<ellipse cx="12" cy="5.4" rx="6.4" ry="2.1" fill="#8a5a2b" />
<circle cx="12" cy="3.2" r="1.05" fill="#8a5a2b" />
<path d="M5.6 7 C5.6 7 5.1 15 6.6 19 C7.4 21 16.6 21 17.4 19 C18.9 15 18.4 7 18.4 7 Z" fill="#e0a94a" stroke="#a9722a" stroke-width="0.5" />
<rect x="5.8" y="7" width="12.4" height="2.1" rx="1" fill="#c6922f" />
<path d="M12 21 C12 21 13 22.4 12 23.2 C11 22.4 12 21 12 21 Z" fill="#c6922f" />
</svg>
<span
id="cart-badge"
style="display:none"
class="absolute -top-2 -right-2 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-brand-ink px-1 text-xs text-white ring-2 ring-white"
></span>
</a>
)}
</nav>
<button
id="menu-toggle"
class="md:hidden inline-flex items-center justify-center rounded-md p-2 text-brand-text"
aria-label="Menü öffnen"
aria-expanded="false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
<nav id="mobile-menu" class="md:hidden hidden pb-4 flex flex-col gap-3">
<div class="flex gap-1 rounded-full bg-brand-surface p-1 mb-2">
{Object.entries(sections).map(([key, s]) => (
<a
href={s.homeHref}
class:list={[
"flex-1 text-center rounded-full px-4 py-1.5 text-sm font-semibold transition-colors",
key === section ? "bg-brand-primary text-brand-ink" : "text-brand-text",
]}
>
{s.label}
</a>
))}
</div>
{config.nav.map((item) => (
<a href={item.href} class="text-sm font-medium text-brand-text hover:text-brand-primary">
{item.label}
</a>
))}
{section === "private" && (
<a href="/warenkorb" class="relative inline-flex items-center gap-2 text-sm font-medium text-brand-text hover:text-brand-primary">
<svg viewBox="0 0 24 24" class="h-6 w-6 shrink-0" aria-hidden="true">
<ellipse cx="12" cy="5.4" rx="6.4" ry="2.1" fill="#8a5a2b" />
<circle cx="12" cy="3.2" r="1.05" fill="#8a5a2b" />
<path d="M5.6 7 C5.6 7 5.1 15 6.6 19 C7.4 21 16.6 21 17.4 19 C18.9 15 18.4 7 18.4 7 Z" fill="#e0a94a" stroke="#a9722a" stroke-width="0.5" />
<rect x="5.8" y="7" width="12.4" height="2.1" rx="1" fill="#c6922f" />
<path d="M12 21 C12 21 13 22.4 12 23.2 C11 22.4 12 21 12 21 Z" fill="#c6922f" />
</svg>
Warenkorb
<span
id="cart-badge-mobile"
style="display:none"
class="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-brand-ink px-1 text-xs text-white"
></span>
</a>
)}
</nav>
</div>
</header>
<script>
const toggle = document.getElementById("menu-toggle");
const menu = document.getElementById("mobile-menu");
toggle?.addEventListener("click", () => {
const isHidden = menu?.classList.contains("hidden");
menu?.classList.toggle("hidden");
toggle.setAttribute("aria-expanded", String(isHidden));
});
</script>
<script>
import { getCart } from "../lib/cart";
import { getCartIdClient } from "../lib/cartCookie";
async function refreshCartBadge() {
const badges = [
document.getElementById("cart-badge"),
document.getElementById("cart-badge-mobile"),
].filter((el): el is HTMLElement => el != null);
if (badges.length === 0) return;
const cartId = getCartIdClient();
if (!cartId) {
badges.forEach((badge) => (badge.style.display = "none"));
return;
}
try {
const cart = await getCart(cartId);
const count = (cart.items ?? []).reduce((sum, item) => sum + item.quantity, 0);
badges.forEach((badge) => {
if (count > 0) {
badge.textContent = String(count);
badge.style.display = "inline-flex";
} else {
badge.style.display = "none";
}
});
} catch {
badges.forEach((badge) => (badge.style.display = "none"));
}
}
refreshCartBadge();
window.addEventListener("cart:updated", refreshCartBadge);
</script>
+20
View File
@@ -0,0 +1,20 @@
---
import { partners } from "../lib/partners";
---
<section class="bg-white">
<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 text-center">Vertrauen uns</p>
<h2 class="mt-2 text-3xl text-brand-ink text-center">Unsere Partner</h2>
<div class="mt-10 grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-8 items-center">
{partners.map((partner) => (
<img
src={`/images/partners/${partner.file}`}
alt={partner.name}
title={partner.name}
class="h-12 w-auto mx-auto object-contain grayscale opacity-70 hover:opacity-100 hover:grayscale-0 transition"
/>
))}
</div>
</div>
</section>
@@ -0,0 +1,70 @@
---
import PartnerGrid from "../PartnerGrid.astro";
---
<section
class="relative bg-cover bg-center"
style="background-image: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.55)), url('/images/business-hero.jpg');"
>
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-24 sm:py-36">
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-brand-accent">Nachhaltig · Professionell · Hamburg</p>
<h1 class="mt-4 text-4xl sm:text-6xl text-white max-w-2xl">
Eigene Bienen vor Ort.
</h1>
<p class="mt-5 text-lg text-white/85 max-w-md normal-case">
Professionell betreut. Nachhaltig erlebbar. Corporate Beekeeping für Unternehmen und Hotels von place4bees.
</p>
<div class="mt-10 flex flex-wrap gap-4">
<a href="/business/anfrage" class="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>
<a href="/business/ihr-projekt" class="rounded border border-white/50 text-white px-7 py-3 text-sm font-semibold hover:bg-white/10 transition-colors">
Ihr Projekt
</a>
</div>
</div>
</section>
<section class="bg-white py-10 flex justify-center">
<img src="/images/logo-badge.png" alt="place4bees Modern Beekeeping, est. 2021" class="h-40 w-auto" />
</section>
<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 grid gap-10 sm:grid-cols-2 items-center">
<div>
<h2 class="text-3xl text-brand-ink">Geben Sie Bienen ein Zuhause</h2>
<p class="mt-4 text-brand-text normal-case">
Zusammen mit place4bees können Unternehmen und Hotels heimischen Honigbienen jetzt einen
geeigneten Lebensraum geben und so einen wertvollen Beitrag zur lokalen Naturvielfalt
leisten. Die Möglichkeiten sind individuell und vielfältig, aber eines haben sie
gemeinsam: ein Nachhaltigkeitserlebnis vor Ort zusammen mit place4bees professionell
betreut und spannend aufbereitet.
</p>
<a href="/business/anfrage" class="mt-6 inline-block rounded bg-brand-primary text-brand-ink px-7 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Starten Sie jetzt Ihr Unternehmensbienen-Projekt
</a>
</div>
<img src="/images/business-hero-2.jpg" alt="Bienenstöcke vor Ort" class="rounded-2xl w-full h-80 object-cover" />
</div>
</section>
<section class="bg-white">
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-16 sm:py-20 grid gap-10 sm:grid-cols-2 items-center">
<img src="/images/bee-on-flower.jpg" alt="Biene auf einer Blüte" class="rounded-2xl w-full h-80 object-cover" />
<div>
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-brand-accent">Zum Hintergrund</p>
<h2 class="mt-2 text-3xl text-brand-ink">Aktiver Bienenschutz</h2>
<p class="mt-4 text-brand-text normal-case">
Bienen nehmen eine Schlüsselrolle in unserem globalen Ökosystem ein. Mit ihrer
Bestäubungsleistung sind sie elementar für die Artenvielfalt. Doch: Die Bienen brauchen
unsere Hilfe. Allein in Europa sterben jedes Jahr zwei Millionen Bienenvölker.
</p>
<p class="mt-4 text-brand-text normal-case">
Ein Unternehmensbienen-Projekt bedeutet erlebbare Nachhaltigkeit vor Ort und aktive
Unterstützung der lokalen Biodiversität.
</p>
</div>
</div>
</section>
<PartnerGrid />
@@ -0,0 +1,42 @@
---
---
<section
class="relative bg-cover bg-center"
style="background-image: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.55)), url('/images/private-hero.jpg');"
>
<div class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-24 sm:py-36">
<p class="text-xs font-semibold uppercase tracking-[0.25em] text-brand-accent">Regional · Nachhaltig · Hamburg</p>
<h1 class="mt-4 text-4xl sm:text-6xl text-white max-w-2xl">
Unsere fleißigen Bienen erzeugen regionalen Honig
</h1>
<p class="mt-5 text-lg text-white/85 max-w-md normal-case">
Einige davon mitten in Hamburg. Frisch aus der Region, direkt von place4bees.
</p>
<div class="mt-10 flex flex-wrap gap-4">
<a href="/shop" class="rounded bg-brand-primary text-brand-ink px-7 py-3 text-sm font-semibold hover:bg-brand-primary-hover transition-colors">
Zum Shop
</a>
<a href="/ueber-uns" class="rounded border border-white/50 text-white px-7 py-3 text-sm font-semibold hover:bg-white/10 transition-colors">
Über uns
</a>
</div>
</div>
</section>
<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 grid gap-10 sm:grid-cols-3">
<div>
<h2 class="text-2xl text-brand-ink">Regionale Honigsorten</h2>
<p class="mt-2 text-sm text-brand-text normal-case">Rapshonig, Frühlingsblüte, Akazie, Wald- und Heidehonig cremig oder flüssig.</p>
</div>
<div>
<h2 class="text-2xl text-brand-ink">Bienenprodukte</h2>
<p class="mt-2 text-sm text-brand-text normal-case">Bienenwachskerzen, reines Bienenwachs und Honigseifen aus eigener Herstellung.</p>
</div>
<div>
<h2 class="text-2xl text-brand-ink">Erlebnis &amp; Patenschaft</h2>
<p class="mt-2 text-sm text-brand-text normal-case">Bienen-Schnupperkurse und Bienenpatenschaften für Privatpersonen.</p>
</div>
</div>
</section>
+51
View File
@@ -0,0 +1,51 @@
---
import "../styles/global.css";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { COMPANY_NAME, PRIMARY_DOMAIN, sections, resolveSection } from "../lib/sections";
interface Props {
title: string;
description: string;
image?: string;
}
const { title, description, image } = Astro.props;
const pathname = Astro.url.pathname;
// Host only matters to resolve the bare "/" root; every other path is
// resolved from the pathname alone, so skip touching request headers there
// (they aren't available on statically prerendered pages).
const hostname = pathname === "/" ? Astro.request.headers.get("host") ?? Astro.url.hostname : "";
const section = resolveSection(pathname, hostname);
// "/" mirrors different content per host; canonicalize it to the matching
// stable section path so both domains consolidate onto one indexable URL.
const canonicalPath = pathname === "/" ? sections[section].homeHref : pathname;
const canonicalUrl = new URL(canonicalPath, PRIMARY_DOMAIN).toString();
---
<html lang="de" data-section={section}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{title} · {COMPANY_NAME}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
{image && <meta property="og:image" content={image} />}
<meta name="twitter:card" content="summary_large_image" />
</head>
<body class="min-h-screen flex flex-col bg-white text-brand-body antialiased">
<Header section={section} />
<main class="flex-1">
<slot />
</main>
<Footer section={section} />
</body>
</html>
+109
View File
@@ -0,0 +1,109 @@
import { medusaFetch } from "./medusa";
export interface MedusaCartItem {
id: string;
title: string;
thumbnail: string | null;
quantity: number;
unit_price: number;
}
export interface MedusaCart {
id: string;
email: string | null;
currency_code: string;
item_total: number;
items: MedusaCartItem[];
}
export interface MedusaShippingOption {
id: string;
name: string;
amount: number;
}
export async function createCart(regionId: string): Promise<MedusaCart> {
const data = await medusaFetch("/store/carts", {
method: "POST",
body: JSON.stringify({ region_id: regionId }),
});
return data.cart;
}
export async function getCart(cartId: string): Promise<MedusaCart> {
const data = await medusaFetch(`/store/carts/${cartId}`);
return data.cart;
}
export async function addLineItem(cartId: string, variantId: string, quantity = 1): Promise<MedusaCart> {
const data = await medusaFetch(`/store/carts/${cartId}/line-items`, {
method: "POST",
body: JSON.stringify({ variant_id: variantId, quantity }),
});
return data.cart;
}
export async function updateLineItem(cartId: string, lineId: string, quantity: number): Promise<MedusaCart> {
const data = await medusaFetch(`/store/carts/${cartId}/line-items/${lineId}`, {
method: "POST",
body: JSON.stringify({ quantity }),
});
return data.cart;
}
export async function removeLineItem(cartId: string, lineId: string): Promise<void> {
await medusaFetch(`/store/carts/${cartId}/line-items/${lineId}`, { method: "DELETE" });
}
interface CartAddress {
first_name: string;
last_name: string;
address_1: string;
postal_code: string;
city: string;
country_code: string;
}
export async function updateCart(
cartId: string,
payload: { email?: string; shipping_address?: CartAddress; billing_address?: CartAddress }
): Promise<MedusaCart> {
const data = await medusaFetch(`/store/carts/${cartId}`, {
method: "POST",
body: JSON.stringify(payload),
});
return data.cart;
}
export async function listShippingOptions(cartId: string): Promise<MedusaShippingOption[]> {
const data = await medusaFetch(`/store/shipping-options?cart_id=${cartId}`);
return data.shipping_options;
}
export async function addShippingMethod(cartId: string, optionId: string): Promise<MedusaCart> {
const data = await medusaFetch(`/store/carts/${cartId}/shipping-methods`, {
method: "POST",
body: JSON.stringify({ option_id: optionId }),
});
return data.cart;
}
export async function createPaymentCollection(cartId: string) {
const data = await medusaFetch("/store/payment-collections", {
method: "POST",
body: JSON.stringify({ cart_id: cartId }),
});
return data.payment_collection;
}
export async function createPaymentSession(paymentCollectionId: string, providerId: string) {
const data = await medusaFetch(`/store/payment-collections/${paymentCollectionId}/payment-sessions`, {
method: "POST",
body: JSON.stringify({ provider_id: providerId }),
});
return data.payment_collection;
}
export async function completeCart(cartId: string): Promise<{ type: "order" | "cart"; order?: any; cart?: any; error?: { message: string } }> {
return medusaFetch(`/store/carts/${cartId}/complete`, { method: "POST" });
}
+14
View File
@@ -0,0 +1,14 @@
export const CART_COOKIE_NAME = "cart_id";
export function getCartIdClient(): string | null {
const match = document.cookie.match(new RegExp(`(?:^|; )${CART_COOKIE_NAME}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
}
export function setCartIdClient(id: string) {
document.cookie = `${CART_COOKIE_NAME}=${encodeURIComponent(id)}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
export function clearCartIdClient() {
document.cookie = `${CART_COOKIE_NAME}=; path=/; max-age=0`;
}
+8
View File
@@ -0,0 +1,8 @@
export function formatEUR(amount: number): string {
return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(amount);
}
export function formatGrundpreis(amount: number, netWeightGrams: number): string {
const perKg = (amount / netWeightGrams) * 1000;
return `${formatEUR(perKg)}/kg`;
}
+67
View File
@@ -0,0 +1,67 @@
const BACKEND_URL = import.meta.env.PUBLIC_MEDUSA_BACKEND_URL;
const PUBLISHABLE_KEY = import.meta.env.PUBLIC_MEDUSA_PUBLISHABLE_KEY;
export async function medusaFetch(path: string, init?: RequestInit) {
const res = await fetch(`${BACKEND_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
"x-publishable-api-key": PUBLISHABLE_KEY,
...init?.headers,
},
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(body?.message || `Medusa request failed: ${path} (${res.status})`);
}
return body;
}
export interface MedusaVariant {
id: string;
title: string;
calculated_price?: { calculated_amount: number; currency_code: string };
metadata?: { net_weight_grams?: number } | null;
}
export interface MedusaProduct {
id: string;
title: string;
handle: string;
description: string | null;
thumbnail: string | null;
images?: { id: string; url: string }[];
variants: MedusaVariant[];
metadata?: { delivery_time?: string; category?: string } | null;
}
// Pulls in the custom metadata (delivery time, category, net fill weight) on
// top of Medusa's default product/variant fields.
const PRODUCT_FIELDS = "*variants.calculated_price,+metadata,+variants.metadata";
export async function listProducts(regionId?: string): Promise<MedusaProduct[]> {
const params = new URLSearchParams({ fields: PRODUCT_FIELDS });
if (regionId) params.set("region_id", regionId);
const data = await medusaFetch(`/store/products?${params}`);
return data.products;
}
export async function getProductByHandle(handle: string, regionId?: string): Promise<MedusaProduct | null> {
const params = new URLSearchParams({ handle, fields: PRODUCT_FIELDS });
if (regionId) params.set("region_id", regionId);
const data = await medusaFetch(`/store/products?${params}`);
return data.products[0] ?? null;
}
export interface MedusaRegion {
id: string;
currency_code: string;
}
export async function getDefaultRegion(): Promise<MedusaRegion> {
const data = await medusaFetch("/store/regions");
return data.regions[0];
}
+35
View File
@@ -0,0 +1,35 @@
export const partners = [
{ name: "ABB", file: "abb-3x2-1.png" },
{ name: "AdvoNeo", file: "advoneo-3x2-1.png" },
{ name: "ak.", file: "ak-3x2-1-smaller.png" },
{ name: "bp", file: "bp-3x2-1.png" },
{ name: "Coloplast", file: "coloplast_logo.png" },
{ name: "dentsu", file: "dentsu-3x2-1.png" },
{ name: "East Cosmos", file: "east-cosmos-3x2-1.png" },
{ name: "Edeka Harburg", file: "edeka_harburg.png" },
{ name: "Eggers", file: "eggers-3x2-1.png" },
{ name: "Empire Riverside Hotels", file: "empire-riverside-hotels-3x2-1.png" },
{ name: "Engel & Völkers", file: "engel-voelkers-3x2-1.png" },
{ name: "EY", file: "ey-3x2-1.png" },
{ name: "Fairplay", file: "fairplay-3x2-1.png" },
{ name: "Hamburger Feuerkasse", file: "hamburger_feuerkasse.png" },
{ name: "Hanseatic Bank", file: "hanseatic-bank-3x2-1.png" },
{ name: "International School of Hamburg", file: "international-school-of-hamburg-3x2-1.png" },
{ name: "Jung von Matt", file: "jungvmatt-3x2-1.png" },
{ name: "Kinderzimmer", file: "kinderzimmer.png" },
{ name: "Kontora", file: "kontora-3x2-1.png" },
{ name: "Lorentz", file: "lorentz-3x2-1.png" },
{ name: "Martha Haus", file: "martha_haus.png" },
{ name: "NKG", file: "nkg-3x2-1.png" },
{ name: "Nordmann", file: "nordmann-3x2-1.png" },
{ name: "Porsche Zentren", file: "porsche_zentren.png" },
{ name: "Private Banking", file: "private-banking-3x2-1.png" },
{ name: "Röder Stiftung", file: "roeder_stiftung.png" },
{ name: "Sund", file: "sund-3x2-1.png" },
{ name: "Stilwerks", file: "stilwerks-3x2-1-smaller.png" },
{ name: "Still", file: "still-3x2-1.png" },
{ name: "Seniorenzentren Geschwister Jensen", file: "senioren-zentren-geschwister-jensen-3x2-1.png" },
{ name: "Tews", file: "tews-3x2-1.png" },
{ name: "The Cloud One", file: "the_cloud_one.png" },
{ name: "ZF", file: "zf-3x2-1.png" },
];
+61
View File
@@ -0,0 +1,61 @@
export type Section = "private" | "business";
export const COMPANY_NAME = "place4bees";
/** Fixed canonical domain: all pages consolidate their SEO signal here, even
* though the site also resolves on hhonig.de (see Layout.astro). */
export const PRIMARY_DOMAIN = "https://place4bees.com";
interface SectionConfig {
label: string;
tagline: string;
/** Stable, host-independent path for this section's homepage. Always link
* here internally (never to bare "/") so switching sections behaves the
* same regardless of which domain a visitor is on. */
homeHref: string;
nav: { label: string; href: string }[];
}
export const sections: Record<Section, SectionConfig> = {
private: {
label: "Privatkunden",
tagline: "Regionaler Honig aus Hamburg",
homeHref: "/privatkunden",
nav: [
{ label: "Shop", href: "/shop" },
{ label: "Über uns", href: "/ueber-uns" },
{ label: "Kontakt", href: "/kontakt" },
],
},
business: {
label: "Business",
tagline: "Eigene Bienen vor Ort. Professionell betreut. Nachhaltig erlebbar.",
homeHref: "/business",
nav: [
{ label: "Ihr Projekt", href: "/business/ihr-projekt" },
{ label: "Über uns", href: "/business/ueber-uns" },
{ label: "Philosophie", href: "/business/philosophie" },
{ label: "Presse", href: "/business/presse" },
{ label: "Angebot", href: "/business/angebot" },
{ label: "Anfrage", href: "/business/anfrage" },
{ label: "Kontakt", href: "/business/kontakt" },
],
},
};
export function sectionForPath(pathname: string): Section {
return pathname.startsWith("/business") ? "business" : "private";
}
/** hhonig.de defaults to the private/B2C section, place4bees.com to business. */
export function isBusinessHost(hostname: string): boolean {
return hostname.includes("place4bees");
}
/** Resolves the active section for a request, accounting for the host-based
* default that only applies at the bare "/" root. */
export function resolveSection(pathname: string, hostname: string): Section {
if (pathname.startsWith("/business")) return "business";
if (pathname === "/" && isBusinessHost(hostname)) return "business";
return "private";
}
+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>
+47
View File
@@ -0,0 +1,47 @@
@import "tailwindcss";
@import "@fontsource/bebas-neue";
@import "@fontsource-variable/open-sans";
/*
* Design tokens adopted from the real place4bees.com corporate site: black
* condensed uppercase headings (Bebas Neue), Open Sans body copy, a soft
* sage-green accent for eyebrow labels, an orange CTA color, and a light
* grey page background instead of the earlier dark-ink/honey-gold theme.
*/
@theme {
--color-brand-ink: #111111;
--color-brand-body: #111111;
--color-brand-text: #333333;
--color-brand-text-muted: #6b6b6b;
--color-brand-primary: #f8ac11;
--color-brand-primary-hover: #d9930a;
--color-brand-accent: #9fc7aa;
--color-brand-accent-hover: #7fae8e;
--color-brand-surface: #f2f3f5;
--color-brand-surface-strong: #ffffff;
--color-brand-border: #e0e1e4;
--color-brand-cream: #ffffff;
--font-display: "Bebas Neue", Arial, Helvetica, sans-serif;
--font-sans: "Open Sans Variable", Arial, Helvetica, sans-serif;
}
body {
font-family: var(--font-sans);
}
h1,
h2,
h3 {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.02em;
text-transform: uppercase;
}
/* Business section swaps the CTA accent to the sage green, so the whole
* page (header, buttons, links) subtly signals which mode is active. */
[data-section="business"] {
--color-brand-primary: var(--color-brand-accent);
--color-brand-primary-hover: var(--color-brand-accent-hover);
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}