first commit
@@ -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/
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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/)
|
||||
@@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
@@ -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).
|
||||
@@ -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'
|
||||
})
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 655 B |
@@ -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 |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
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>
|
||||
@@ -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">© {year} {COMPANY_NAME}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -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>
|
||||
@@ -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 & Patenschaft</h2>
|
||||
<p class="mt-2 text-sm text-brand-text normal-case">Bienen-Schnupperkurse und Bienenpatenschaften für Privatpersonen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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>
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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" },
|
||||
];
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 & 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">1–2 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 & 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 & 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||