Demoshop Directus+Astro — Hofladen Heidekorn (Katalog, Warenkorb, Magazin, Rechtstexte)

This commit is contained in:
2026-06-16 03:36:53 +00:00
commit 87fda0927e
21 changed files with 6072 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.astro
.git
+3
View File
@@ -0,0 +1,3 @@
node_modules
dist
.astro
+12
View File
@@ -0,0 +1,12 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ARG DIRECTUS_URL
ENV DIRECTUS_URL=$DIRECTUS_URL
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
+5
View File
@@ -0,0 +1,5 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
build: { format: 'directory' }
});
+8
View File
@@ -0,0 +1,8 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / { try_files $uri $uri/ $uri.html /index.html; }
location ~* \.(css|js|woff2|jpg|png|svg|webp)$ { expires 7d; add_header Cache-Control "public"; }
}
+5506
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "demo-directus-storefront",
"type": "module",
"version": "1.0.0",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^4.16.18",
"@fontsource-variable/fraunces": "^5.1.0",
"@fontsource-variable/public-sans": "^5.1.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
.legal{max-width:740px;padding-top:30px;padding-bottom:20px}
.legal h1{font-size:clamp(1.8rem,3.5vw,2.6rem);margin:.3em 0 .6em}
.legal h3{margin:1.4em 0 .3em;font-size:1.15rem}
.legal p{color:var(--graphit);margin:0 0 .9em}
.demo-note{display:inline-block;background:oklch(0.92 0.06 80);color:oklch(0.4 0.08 60);font-size:.8rem;font-weight:600;padding:6px 14px;border-radius:8px;margin-bottom:8px}
.legal .src{margin-top:24px;padding-top:16px;border-top:1px solid var(--line);font-size:.82rem;color:var(--slate)}
+1
View File
@@ -0,0 +1 @@
{"data":[{"id":1,"titel":"Warum Heidehonig so besonders schmeckt","slug":"heidehonig-besonders","datum":"2026-05-12","teaser":"Ein Blick in die Imkerei und auf das, was den Heidehonig von anderen unterscheidet.","inhalt":"Heidehonig entsteht spät im Jahr, wenn die Besenheide blüht. Seine gelierende Konsistenz und das würzige Aroma machen ihn unter Kennern begehrt...\n\nUnsere Imker arbeiten in kleinen Chargen und ernten nur, was die Völker entbehren können.","bild_url":"https://picsum.photos/seed/journal-honig/1200/700"},{"id":2,"titel":"Grünkohlsaison: Tradition trifft Hofladen","slug":"gruenkohlsaison","datum":"2026-04-28","teaser":"Wenn der erste Frost kommt, beginnt im Norden die Kohlsaison. Wir zeigen, was dazugehört.","inhalt":"Kein norddeutscher Winter ohne Grünkohl. Dazu Mettwurst, Kassler und Senf...\n\nIm Hofladen bekommst du alles für den perfekten Kohlgang regional und frisch.","bild_url":"https://picsum.photos/seed/journal-kohl/1200/700"},{"id":3,"titel":"Streuobstwiesen: Saft mit Geschichte","slug":"streuobstwiesen","datum":"2026-03-15","teaser":"Alte Apfelsorten, gepresst zu naturtrübem Direktsaft. Warum sich der Erhalt lohnt.","inhalt":"Streuobstwiesen sind Lebensraum für hunderte Arten. Mit jedem Glas Saft unterstützt du ihren Erhalt...\n\nUnser Apfelsaft stammt zu 100 % von regionalen Wiesen.","bild_url":"https://picsum.photos/seed/journal-saft/1200/700"}]}
+1
View File
@@ -0,0 +1 @@
{"data":[{"id":1,"name":"Heidehonig im Glas","slug":"heidehonig","kategorie":"Honig","preis":8.9,"mwst":7,"einheit":"250 g","kurzbeschreibung":"Kräftiger Honig von Heideblüten aus der Lüneburger Heide.","beschreibung":"Unser Heidehonig wird von Imkern aus der Region geschleudert. Würzig-aromatisch, leicht gelierend ideal aufs Frühstücksbrot.","bild_url":"https://picsum.photos/seed/heidehonig/900/700","badge":"Bestseller","lagernd":1},{"id":2,"name":"Buchweizenhonig","slug":"buchweizenhonig","kategorie":"Honig","preis":9.5,"mwst":7,"einheit":"250 g","kurzbeschreibung":"Dunkel, malzig, charaktervoll.","beschreibung":"Ein außergewöhnlicher Honig mit malzig-herber Note. Selten und in kleinen Chargen abgefüllt.","bild_url":"https://picsum.photos/seed/buchweizen/900/700","badge":"Limitiert","lagernd":1},{"id":3,"name":"Wildkräutertee „Heideluft“","slug":"wildkraeutertee","kategorie":"Tee","preis":6.4,"mwst":7,"einheit":"80 g","kurzbeschreibung":"Lose Teemischung aus regionalen Wildkräutern.","beschreibung":"Handgepflückte Kräuter, schonend getrocknet. Mild, blumig, koffeinfrei.","bild_url":"https://picsum.photos/seed/heideluft/900/700","badge":"Bio","lagernd":1},{"id":4,"name":"Schwarztee „Ostfriesische Mischung“","slug":"ostfriesentee","kategorie":"Tee","preis":7.2,"mwst":7,"einheit":"100 g","kurzbeschreibung":"Kräftige Assam-Broken-Mischung.","beschreibung":"Klassisch ostfriesisch: kräftig, malzig, mit Kandis und Sahne ein Genuss.","bild_url":"https://picsum.photos/seed/ostfriesen/900/700","badge":"","lagernd":1},{"id":5,"name":"Heidschnucken-Salami","slug":"heidschnucken-salami","kategorie":"Wurst & Fleisch","preis":12.9,"mwst":7,"einheit":"200 g","kurzbeschreibung":"Luftgetrocknet, würzig, von Heidschnucken.","beschreibung":"Spezialität aus der Heide. Langsam gereift, kräftig im Geschmack, ohne Zusatzstoffe.","bild_url":"https://picsum.photos/seed/salami/900/700","badge":"Regional","lagernd":1},{"id":6,"name":"Grünkohl-Mettwurst","slug":"gruenkohl-mettwurst","kategorie":"Wurst & Fleisch","preis":5.8,"mwst":7,"einheit":"Glas 250 g","kurzbeschreibung":"Die norddeutsche Klassikerin für die Kohlsaison.","beschreibung":"Grobe Mettwurst, perfekt zum Grünkohl. Nach traditionellem Hausrezept.","bild_url":"https://picsum.photos/seed/mettwurst/900/700","badge":"","lagernd":1},{"id":7,"name":"Sanddorn-Fruchtaufstrich","slug":"sanddorn-aufstrich","kategorie":"Marmelade","preis":4.9,"mwst":7,"einheit":"225 g","kurzbeschreibung":"Fruchtig-herb mit viel Vitamin C.","beschreibung":"Sanddorn von der Küste, schonend eingekocht mit 60 % Fruchtanteil.","bild_url":"https://picsum.photos/seed/sanddorn/900/700","badge":"Neu","lagernd":1},{"id":8,"name":"Brombeer-Gelee","slug":"brombeer-gelee","kategorie":"Marmelade","preis":4.5,"mwst":7,"einheit":"225 g","kurzbeschreibung":"Aus wilden Heckenbrombeeren.","beschreibung":"Von Hand gepflückte Brombeeren aus regionalen Hecken, fein passiert.","bild_url":"https://picsum.photos/seed/brombeer/900/700","badge":"","lagernd":0},{"id":9,"name":"Apfelsaft naturtrüb (Streuobst)","slug":"apfelsaft-streuobst","kategorie":"Getränke","preis":3.2,"mwst":19,"einheit":"0,7 l","kurzbeschreibung":"Direktsaft von Streuobstwiesen.","beschreibung":"100 % Direktsaft, ungefiltert, von alten Apfelsorten der Region. Pfandflasche.","bild_url":"https://picsum.photos/seed/apfelsaft/900/700","badge":"","lagernd":1},{"id":10,"name":"Heide-Likör „Goldherz“","slug":"heide-likoer","kategorie":"Getränke","preis":15.9,"mwst":19,"einheit":"0,35 l","kurzbeschreibung":"Feiner Kräuterlikör, 28 % vol.","beschreibung":"Handangesetzter Likör mit Heidekräutern und Honig. Ein rundes Finale für jeden Abend.","bild_url":"https://picsum.photos/seed/likoer/900/700","badge":"Geschenkidee","lagernd":1}]}
+1
View File
@@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />
+181
View File
@@ -0,0 +1,181 @@
---
import '@fontsource-variable/fraunces';
import '@fontsource-variable/public-sans';
const { title = 'Hofladen Heidekorn', desc = 'Regionale Spezialitäten aus der Lüneburger Heide — Demoshop.' } = Astro.props;
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title} · Hofladen Heidekorn</title>
<meta name="description" content={desc} />
<meta name="robots" content="noindex" />
<style is:global>
:root{
--petrol: oklch(0.50 0.075 205);
--petrol-deep: oklch(0.38 0.07 212);
--light: oklch(0.975 0.008 200);
--soft: oklch(0.93 0.018 195);
--slate: oklch(0.55 0.02 230);
--graphit: oklch(0.33 0.012 240);
--ink: oklch(0.21 0.012 250);
--weiss: oklch(0.995 0 0);
--line: oklch(0.88 0.012 215);
--radius: 14px;
--wrap: 1140px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--light);color:var(--ink);
font-family:'Public Sans Variable',system-ui,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased}
h1,h2,h3{font-family:'Fraunces Variable',Georgia,serif;font-weight:560;line-height:1.12;letter-spacing:-0.01em;margin:0 0 .4em}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
.wrap{max-width:var(--wrap);margin-inline:auto;padding-inline:22px}
.u{position:relative;display:inline-block}
.u::after{content:"_";color:var(--petrol);font-weight:700}
.btn{display:inline-flex;align-items:center;gap:.5em;background:var(--petrol);color:var(--weiss);
border:none;border-radius:999px;padding:.7em 1.3em;font:inherit;font-weight:600;cursor:pointer;transition:background .18s,transform .18s}
.btn:hover{background:var(--petrol-deep);transform:translateY(-1px)}
.btn.ghost{background:transparent;color:var(--petrol);border:1.5px solid var(--line)}
.btn.ghost:hover{border-color:var(--petrol);background:transparent}
/* header */
header.site{position:sticky;top:0;z-index:40;background:color-mix(in oklch,var(--light) 86%, transparent);
backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.nav{display:flex;align-items:center;justify-content:space-between;height:68px}
.brand{font-family:'Fraunces Variable',serif;font-size:1.3rem;font-weight:600}
.brand small{display:block;font-family:'Public Sans Variable',sans-serif;font-size:.62rem;letter-spacing:.18em;
text-transform:uppercase;color:var(--slate);font-weight:600}
.nav-links{display:flex;gap:1.6rem;align-items:center;font-weight:500}
.nav-links a:hover{color:var(--petrol)}
.cart-btn{position:relative;background:none;border:none;cursor:pointer;color:var(--ink);font:inherit;display:flex;align-items:center;gap:.4rem;font-weight:600}
.cart-badge{position:absolute;top:-8px;right:-10px;background:var(--petrol);color:#fff;border-radius:999px;
min-width:18px;height:18px;font-size:.7rem;display:grid;place-items:center;padding:0 4px}
/* drawer */
.drawer-bg{position:fixed;inset:0;background:oklch(0.2 0.02 250/.45);opacity:0;pointer-events:none;transition:opacity .25s;z-index:50}
.drawer-bg.open{opacity:1;pointer-events:auto}
.drawer{position:fixed;top:0;right:0;height:100%;width:min(420px,92vw);background:var(--weiss);z-index:60;
transform:translateX(100%);transition:transform .28s ease;display:flex;flex-direction:column;box-shadow:-12px 0 40px oklch(0.2 0.02 250/.18)}
.drawer.open{transform:none}
.drawer header{display:flex;justify-content:space-between;align-items:center;padding:20px;border-bottom:1px solid var(--line)}
.drawer h3{margin:0;font-size:1.15rem}
.drawer .items{flex:1;overflow:auto;padding:8px 20px}
.ci{display:flex;gap:12px;padding:14px 0;border-bottom:1px solid var(--line)}
.ci img{width:62px;height:62px;object-fit:cover;border-radius:10px;flex:none}
.ci .meta{flex:1;min-width:0}
.ci .nm{font-weight:600;font-size:.92rem}
.ci .qty{display:flex;align-items:center;gap:8px;margin-top:6px}
.ci .qty button{width:26px;height:26px;border:1px solid var(--line);background:var(--light);border-radius:7px;cursor:pointer;font-size:1rem;line-height:1}
.ci .rm{background:none;border:none;color:var(--slate);cursor:pointer;font-size:.78rem;text-decoration:underline}
.drawer footer{padding:18px 20px;border-top:1px solid var(--line)}
.sumline{display:flex;justify-content:space-between;margin-bottom:4px;color:var(--graphit);font-size:.9rem}
.sumline.total{color:var(--ink);font-weight:700;font-size:1.1rem;margin-top:8px}
.empty{color:var(--slate);text-align:center;padding:40px 0}
.close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--slate);line-height:1}
/* footer */
footer.site{margin-top:80px;background:var(--graphit);color:oklch(0.9 0.01 220);padding:54px 0 30px}
footer.site a{color:oklch(0.9 0.01 220)}
footer.site a:hover{color:#fff}
.fgrid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:32px}
footer h4{font-family:'Public Sans Variable',sans-serif;text-transform:uppercase;letter-spacing:.12em;font-size:.72rem;color:oklch(0.72 0.02 220);margin:0 0 12px}
.fcol a{display:block;padding:3px 0;font-size:.92rem}
.fnote{border-top:1px solid oklch(0.45 0.01 240);margin-top:34px;padding-top:18px;font-size:.8rem;color:oklch(0.72 0.02 220);display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
.demo-pill{display:inline-block;background:var(--petrol);color:#fff;font-size:.7rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:3px 10px;border-radius:999px}
@media(max-width:760px){.fgrid{grid-template-columns:1fr}.nav-links a:not(.cart-link){display:none}}
.legal{max-width:740px;padding-top:30px;padding-bottom:20px}
.legal h1{font-size:clamp(1.8rem,3.5vw,2.6rem);margin:.3em 0 .6em}
.legal h3{margin:1.4em 0 .3em;font-size:1.15rem}
.legal p{color:var(--graphit);margin:0 0 .9em}
.demo-note{display:inline-block;background:oklch(0.92 0.06 80);color:oklch(0.4 0.08 60);font-size:.8rem;font-weight:600;padding:6px 14px;border-radius:8px;margin-bottom:8px}
.legal .src{margin-top:24px;padding-top:16px;border-top:1px solid var(--line);font-size:.82rem;color:var(--slate)}
</style>
</head>
<body>
<header class="site"><div class="wrap nav">
<a href="/" class="brand">Hofladen Heidekorn<small>Lüneburger Heide</small></a>
<nav class="nav-links">
<a href="/#shop">Shop</a>
<a href="/#magazin">Magazin</a>
<button class="cart-btn" id="cartOpen" aria-label="Warenkorb öffnen">
Warenkorb <span class="cart-badge" id="cartBadge">0</span>
</button>
</nav>
</div></header>
<main><slot /></main>
<div class="drawer-bg" id="drawerBg"></div>
<aside class="drawer" id="drawer" aria-label="Warenkorb">
<header><h3>Dein Warenkorb</h3><button class="close" id="cartClose">×</button></header>
<div class="items" id="cartItems"></div>
<footer>
<div class="sumline"><span>Zwischensumme</span><span id="sumNet">0,00 €</span></div>
<div class="sumline"><span>inkl. MwSt.</span><span id="sumTax">0,00 €</span></div>
<div class="sumline total"><span>Gesamt</span><span id="sumTotal">0,00 €</span></div>
<button class="btn" style="width:100%;justify-content:center;margin-top:14px" id="checkoutBtn">Zur Kasse (Demo)</button>
<p style="font-size:.74rem;color:var(--slate);text-align:center;margin:.7em 0 0">Demoshop · keine echte Bestellung möglich</p>
</footer>
</aside>
<footer class="site"><div class="wrap">
<div class="fgrid">
<div>
<div class="brand" style="color:#fff">Hofladen Heidekorn<small>Lüneburger Heide</small></div>
<p style="max-width:34ch;color:oklch(0.78 0.02 220);font-size:.92rem;margin-top:14px">
Regionale Spezialitäten, direkt vom Hof. Dieser Shop ist ein technischer Demonstrator.</p>
<span class="demo-pill">Demo · Directus + Astro</span>
</div>
<div class="fcol"><h4>Shop</h4>
<a href="/#shop">Alle Produkte</a><a href="/#magazin">Magazin</a><a href="/warenkorb">Warenkorb</a>
</div>
<div class="fcol"><h4>Rechtliches</h4>
<a href="/impressum">Impressum</a><a href="/datenschutz">Datenschutz</a><a href="/agb">AGB</a><a href="/widerruf">Widerruf</a>
</div>
</div>
<div class="fnote"><span>© 2026 Hofladen Heidekorn (Demo) · Heidrich Digital</span><span>Preise inkl. MwSt., zzgl. Versand</span></div>
</div></footer>
<script is:inline>
const KEY='heidekorn_cart';
const load=()=>{try{return JSON.parse(localStorage.getItem(KEY))||[]}catch{return[]}};
const save=c=>localStorage.setItem(KEY,JSON.stringify(c));
const eur=n=>new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR'}).format(n);
let cart=load();
function add(p){const f=cart.find(i=>i.slug===p.slug);if(f)f.qty++;else cart.push({...p,qty:1});save(cart);render();open()}
function setQty(slug,d){const f=cart.find(i=>i.slug===slug);if(!f)return;f.qty+=d;if(f.qty<=0)cart=cart.filter(i=>i.slug!==slug);save(cart);render()}
function remove(slug){cart=cart.filter(i=>i.slug!==slug);save(cart);render()}
function totals(){let total=0,tax=0;cart.forEach(i=>{const line=i.preis*i.qty;total+=line;const r=(i.mwst||19)/100;tax+=line-(line/(1+r))});return{total,tax,net:total-tax}}
function render(){
const badge=document.getElementById('cartBadge');
const n=cart.reduce((s,i)=>s+i.qty,0);
if(badge)badge.textContent=n;
const wrap=document.getElementById('cartItems');
if(wrap){
if(!cart.length){wrap.innerHTML='<p class="empty">Dein Warenkorb ist leer.</p>';}
else{wrap.innerHTML=cart.map(i=>`<div class="ci"><img src="${i.bild_url}" alt=""><div class="meta"><div class="nm">${i.name}</div><div style="color:var(--slate);font-size:.8rem">${eur(i.preis)} · ${i.einheit||''}</div><div class="qty"><button data-dec="${i.slug}"></button><span>${i.qty}</span><button data-inc="${i.slug}">+</button><button class="rm" data-rm="${i.slug}">entfernen</button></div></div><div style="font-weight:600">${eur(i.preis*i.qty)}</div></div>`).join('');}
}
const t=totals();
const set=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=eur(v)};
set('sumNet',t.net);set('sumTax',t.tax);set('sumTotal',t.total);
document.dispatchEvent(new CustomEvent('cart:changed',{detail:{cart,totals:t}}));
}
const drawer=document.getElementById('drawer'),bg=document.getElementById('drawerBg');
function open(){drawer&&drawer.classList.add('open');bg&&bg.classList.add('open')}
function close(){drawer&&drawer.classList.remove('open');bg&&bg.classList.remove('open')}
document.getElementById('cartOpen')?.addEventListener('click',open);
document.getElementById('cartClose')?.addEventListener('click',close);
bg?.addEventListener('click',close);
document.getElementById('checkoutBtn')?.addEventListener('click',()=>{alert('Demoshop — hier würde der Checkout starten. Es findet keine echte Bestellung statt.')});
document.addEventListener('click',e=>{
const a=e.target.closest('[data-add]');
if(a){e.preventDefault();add(JSON.parse(a.getAttribute('data-add')));return}
const inc=e.target.closest('[data-inc]');if(inc){setQty(inc.getAttribute('data-inc'),1);return}
const dec=e.target.closest('[data-dec]');if(dec){setQty(dec.getAttribute('data-dec'),-1);return}
const rm=e.target.closest('[data-rm]');if(rm){remove(rm.getAttribute('data-rm'));return}
});
window.HK={add,setQty,remove,get:()=>cart,totals};
render();
</script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
import produkteSnap from '../data/produkte.json';
import artikelSnap from '../data/artikel.json';
const BASE = import.meta.env.DIRECTUS_URL || 'http://i5yq3bxu3de6kkk7jd2e0nk7.46.224.49.66.sslip.io';
async function tryFetch(path, fallback) {
try {
const res = await fetch(`${BASE}${path}`, { signal: AbortSignal.timeout(8000) });
if (!res.ok) throw new Error('status ' + res.status);
const json = await res.json();
if (json && Array.isArray(json.data) && json.data.length) return json.data;
throw new Error('empty');
} catch (e) {
console.warn('[data] Directus nicht erreichbar, nutze Snapshot:', e.message);
return fallback.data;
}
}
export const getProdukte = () => tryFetch('/items/produkte?limit=-1&sort=id', produkteSnap);
export const getArtikel = () => tryFetch('/items/artikel?limit=-1&sort=-datum', artikelSnap);
export function eur(n) {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(Number(n));
}
+14
View File
@@ -0,0 +1,14 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="AGB">
<article class="wrap legal">
<span class="demo-note">⚠ Demo-Inhalt · Platzhaltertexte ohne Rechtswirkung</span>
<h1>Allgemeine Geschäftsbedingungen</h1>
<h3>§ 1 Geltungsbereich</h3><p>Diese AGB gelten für alle Bestellungen über diesen (Demo-)Onlineshop.</p>
<h3>§ 2 Vertragspartner</h3><p>Der Kaufvertrag kommt zustande mit der Hofladen Heidekorn (Musterbetrieb).</p>
<h3>§ 3 Preise &amp; Versand</h3><p>Alle Preise verstehen sich inkl. der gesetzlichen MwSt. zzgl. Versandkosten. In dieser Demo findet keine echte Bestellung statt.</p>
<h3>§ 4 Zahlung</h3><p>In der Live-Version stehen gängige Zahlarten zur Verfügung. In der Demo ist der Checkout deaktiviert.</p>
<p class="src">Platzhalter-AGB für Demonstrationszwecke.</p>
</article>
</Base>
+14
View File
@@ -0,0 +1,14 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Datenschutz">
<article class="wrap legal">
<span class="demo-note">⚠ Demo-Inhalt · Platzhaltertexte ohne Rechtswirkung</span>
<h1>Datenschutzerklärung</h1>
<h3>1. Verantwortlicher</h3><p>Hofladen Heidekorn (Musterbetrieb), Musterhofweg 1, 21255 Musterheide.</p>
<h3>2. Hosting</h3><p>Diese Demo wird auf Servern in Deutschland (Hetzner, Nürnberg) betrieben. Es werden keine personenbezogenen Daten zu Werbezwecken verarbeitet.</p>
<h3>3. Warenkorb</h3><p>Der Warenkorb wird ausschließlich lokal in deinem Browser (localStorage) gespeichert. Es werden keine Bestell- oder Zahlungsdaten an einen Server übermittelt.</p>
<h3>4. Deine Rechte</h3><p>Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit und Widerspruch gemäß DSGVO.</p>
<p class="src">Platzhalter-Datenschutzerklärung für Demonstrationszwecke.</p>
</article>
</Base>
+14
View File
@@ -0,0 +1,14 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Impressum">
<article class="wrap legal">
<span class="demo-note">⚠ Demo-Inhalt · Platzhaltertexte ohne Rechtswirkung</span>
<h1>Impressum</h1>
<p>Angaben gemäß § 5 DDG</p>
<p><strong>Hofladen Heidekorn (Musterbetrieb)</strong><br/>Musterhofweg 1<br/>21255 Musterheide</p>
<p>Vertreten durch: Max Mustermann<br/>Telefon: 0123 456789<br/>E-Mail: kontakt@example.de</p>
<p>Umsatzsteuer-ID: DE000000000<br/>Inhaltlich verantwortlich: Max Mustermann (Anschrift wie oben)</p>
<p class="src">Technische Umsetzung: Heidrich Digital · Demoshop (Directus + Astro)</p>
</article>
</Base>
+122
View File
@@ -0,0 +1,122 @@
---
import Base from '../layouts/Base.astro';
import { getProdukte, getArtikel, eur } from '../lib/data.js';
const produkte = await getProdukte();
const artikel = await getArtikel();
const kategorien = [...new Set(produkte.map(p => p.kategorie))];
---
<Base>
<section class="hero">
<div class="wrap hero-in">
<div>
<span class="eyebrow">Direkt vom Hof · <span class="u">Lüneburger Heide</span></span>
<h1>Regionale Spezialitäten,<br/>ehrlich gemacht.</h1>
<p class="lead">Honig, Tee, Wurst und mehr — von Erzeugern aus der Region, sorgfältig ausgewählt. Heute bestellt, morgen auf dem Tisch.</p>
<div class="hero-cta">
<a href="#shop" class="btn">Zum Sortiment</a>
<a href="#magazin" class="btn ghost">Magazin lesen</a>
</div>
</div>
<div class="hero-card">
<img src="https://picsum.photos/seed/hofladen-hero/720/820" alt="Hofladen" loading="eager"/>
</div>
</div>
</section>
<section id="shop" class="wrap">
<div class="sec-head">
<h2>Unser Sortiment</h2>
<div class="filters">
<button class="chip active" data-cat="all">Alle</button>
{kategorien.map(k => <button class="chip" data-cat={k}>{k}</button>)}
</div>
</div>
<div class="grid">
{produkte.map(p => (
<article class="card" data-cat={p.kategorie}>
<a href={`/produkt/${p.slug}`} class="thumb">
<img src={p.bild_url} alt={p.name} loading="lazy"/>
{p.badge && <span class="badge">{p.badge}</span>}
{!p.lagernd && <span class="badge out">Ausverkauft</span>}
</a>
<div class="card-body">
<span class="cat">{p.kategorie}</span>
<h3><a href={`/produkt/${p.slug}`}>{p.name}</a></h3>
<p class="kurz">{p.kurzbeschreibung}</p>
<div class="card-foot">
<div><strong class="price">{eur(p.preis)}</strong><span class="einheit">{p.einheit}</span></div>
{p.lagernd
? <button class="btn add" data-add={JSON.stringify({slug:p.slug,name:p.name,preis:p.preis,bild_url:p.bild_url,einheit:p.einheit,mwst:p.mwst})}>In den Korb</button>
: <button class="btn" disabled style="opacity:.5;cursor:not-allowed">Vergriffen</button>}
</div>
</div>
</article>
))}
</div>
</section>
<section id="magazin" class="wrap" style="margin-top:72px">
<div class="sec-head"><h2>Aus dem Magazin</h2></div>
<div class="mag-grid">
{artikel.map(a => (
<a href={`/magazin/${a.slug}`} class="mag">
<div class="mag-img"><img src={a.bild_url} alt={a.titel} loading="lazy"/></div>
<div class="mag-body">
<time>{new Date(a.datum).toLocaleDateString('de-DE',{day:'2-digit',month:'long',year:'numeric'})}</time>
<h3>{a.titel}</h3>
<p>{a.teaser}</p>
<span class="more">Weiterlesen →</span>
</div>
</a>
))}
</div>
</section>
<style>
.hero{background:linear-gradient(180deg,var(--soft),var(--light));padding:54px 0 30px}
.hero-in{display:grid;grid-template-columns:1.1fr .9fr;gap:48px;align-items:center}
.eyebrow{font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:var(--slate);font-weight:700}
.hero h1{font-size:clamp(2.2rem,5vw,3.6rem);margin:.3em 0 .2em}
.lead{font-size:1.12rem;color:var(--graphit);max-width:46ch}
.hero-cta{display:flex;gap:12px;margin-top:24px;flex-wrap:wrap}
.hero-card img{border-radius:20px;width:100%;aspect-ratio:7/8;object-fit:cover;box-shadow:0 30px 60px oklch(0.3 0.04 230/.18)}
.sec-head{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;margin:56px 0 26px;flex-wrap:wrap}
.sec-head h2{font-size:clamp(1.7rem,3vw,2.3rem);margin:0}
.filters{display:flex;gap:8px;flex-wrap:wrap}
.chip{border:1.5px solid var(--line);background:var(--weiss);border-radius:999px;padding:.4em 1em;font:inherit;font-size:.86rem;cursor:pointer;color:var(--graphit);transition:.16s}
.chip:hover{border-color:var(--petrol)}
.chip.active{background:var(--petrol);color:#fff;border-color:var(--petrol)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:24px}
.card{background:var(--weiss);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column;transition:transform .2s,box-shadow .2s}
.card:hover{transform:translateY(-4px);box-shadow:0 18px 40px oklch(0.3 0.03 230/.12)}
.thumb{position:relative;display:block;aspect-ratio:5/4;overflow:hidden}
.thumb img{width:100%;height:100%;object-fit:cover;transition:transform .35s}
.card:hover .thumb img{transform:scale(1.05)}
.badge{position:absolute;top:12px;left:12px;background:var(--petrol);color:#fff;font-size:.72rem;font-weight:700;padding:4px 10px;border-radius:999px}
.badge.out{background:var(--graphit);left:auto;right:12px}
.card-body{padding:16px 18px 18px;display:flex;flex-direction:column;flex:1}
.cat{font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:var(--slate);font-weight:700}
.card-body h3{font-size:1.18rem;margin:.3em 0 .25em}
.kurz{color:var(--graphit);font-size:.9rem;flex:1}
.card-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px}
.price{font-size:1.18rem}.einheit{display:block;font-size:.72rem;color:var(--slate)}
.add{padding:.55em 1em;font-size:.88rem}
.mag-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:24px}
.mag{background:var(--weiss);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;display:flex;flex-direction:column;transition:.2s}
.mag:hover{transform:translateY(-4px);box-shadow:0 18px 40px oklch(0.3 0.03 230/.12)}
.mag-img{aspect-ratio:16/9;overflow:hidden}.mag-img img{width:100%;height:100%;object-fit:cover}
.mag-body{padding:18px 20px}
.mag-body time{font-size:.76rem;color:var(--slate);text-transform:uppercase;letter-spacing:.08em}
.mag-body h3{font-size:1.25rem;margin:.35em 0 .3em}
.mag-body p{color:var(--graphit);font-size:.92rem}
.more{color:var(--petrol);font-weight:600;font-size:.9rem}
@media(max-width:820px){.hero-in{grid-template-columns:1fr}.hero-card{order:-1}}
</style>
<script is:inline>
document.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>{
document.querySelectorAll('.chip').forEach(x=>x.classList.remove('active'));c.classList.add('active');
const cat=c.dataset.cat;
document.querySelectorAll('.card').forEach(card=>{card.style.display=(cat==='all'||card.dataset.cat===cat)?'':'none'});
}));
</script>
</Base>
+28
View File
@@ -0,0 +1,28 @@
---
import Base from '../../layouts/Base.astro';
import { getArtikel } from '../../lib/data.js';
export async function getStaticPaths(){
const artikel = await getArtikel();
return artikel.map(a => ({ params:{ slug:a.slug }, props:{ a } }));
}
const { a } = Astro.props;
---
<Base title={a.titel}>
<article class="wrap article">
<a href="/#magazin" class="back">← Zurück zum Magazin</a>
<time>{new Date(a.datum).toLocaleDateString('de-DE',{day:'2-digit',month:'long',year:'numeric'})}</time>
<h1>{a.titel}</h1>
<p class="teaser">{a.teaser}</p>
<img src={a.bild_url} alt={a.titel}/>
<div class="body">{a.inhalt.split('\n').map(p => p.trim() && <p>{p}</p>)}</div>
</article>
<style>
.article{max-width:760px;padding-top:26px}
.back{color:var(--slate);font-weight:600;font-size:.9rem}
.article time{display:block;margin-top:18px;color:var(--slate);text-transform:uppercase;letter-spacing:.08em;font-size:.8rem}
.article h1{font-size:clamp(2rem,4vw,3rem);margin:.2em 0 .3em}
.teaser{font-size:1.2rem;color:var(--graphit)}
.article img{width:100%;border-radius:16px;margin:24px 0;aspect-ratio:16/9;object-fit:cover}
.body p{font-size:1.05rem;color:var(--ink);margin:0 0 1.1em}
</style>
</Base>
+68
View File
@@ -0,0 +1,68 @@
---
import Base from '../../layouts/Base.astro';
import { getProdukte, eur } from '../../lib/data.js';
export async function getStaticPaths(){
const produkte = await getProdukte();
return produkte.map(p => ({ params:{ slug:p.slug }, props:{ p, produkte } }));
}
const { p, produkte } = Astro.props;
const mehr = produkte.filter(x => x.kategorie===p.kategorie && x.slug!==p.slug).slice(0,3);
---
<Base title={p.name}>
<div class="wrap" style="padding-top:26px">
<a href="/#shop" class="back">← Zurück zum Shop</a>
<div class="detail">
<div class="d-img"><img src={p.bild_url} alt={p.name}/>{p.badge && <span class="badge">{p.badge}</span>}</div>
<div class="d-info">
<span class="cat">{p.kategorie}</span>
<h1>{p.name}</h1>
<p class="kurz">{p.kurzbeschreibung}</p>
<div class="priceblock">
<strong>{eur(p.preis)}</strong>
<span>{p.einheit} · inkl. {p.mwst}% MwSt.</span>
</div>
<p class="stock">{p.lagernd ? '✓ Auf Lager — sofort lieferbar' : '✗ Derzeit ausverkauft'}</p>
{p.lagernd
? <button class="btn big" data-add={JSON.stringify({slug:p.slug,name:p.name,preis:p.preis,bild_url:p.bild_url,einheit:p.einheit,mwst:p.mwst})}>In den Warenkorb</button>
: <button class="btn big" disabled style="opacity:.5">Vergriffen</button>}
<div class="desc"><h3>Produktbeschreibung</h3><p>{p.beschreibung}</p></div>
</div>
</div>
{mehr.length>0 && (
<section style="margin-top:64px">
<h2>Das könnte dir auch schmecken</h2>
<div class="rel">
{mehr.map(m => (
<a href={`/produkt/${m.slug}`} class="relcard">
<img src={m.bild_url} alt={m.name}/>
<div><span class="cat">{m.kategorie}</span><strong>{m.name}</strong><span class="p">{eur(m.preis)}</span></div>
</a>
))}
</div>
</section>
)}
</div>
<style>
.back{color:var(--slate);font-weight:600;font-size:.9rem}
.detail{display:grid;grid-template-columns:1fr 1fr;gap:46px;margin-top:18px}
.d-img{position:relative;border-radius:18px;overflow:hidden}
.d-img img{width:100%;aspect-ratio:5/4;object-fit:cover}
.d-img .badge{position:absolute;top:14px;left:14px;background:var(--petrol);color:#fff;font-size:.74rem;font-weight:700;padding:4px 12px;border-radius:999px}
.cat{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--slate);font-weight:700}
.d-info h1{font-size:clamp(1.8rem,3.5vw,2.6rem);margin:.2em 0 .3em}
.kurz{font-size:1.08rem;color:var(--graphit)}
.priceblock{display:flex;align-items:baseline;gap:12px;margin:18px 0 6px}
.priceblock strong{font-size:1.9rem;font-family:'Fraunces Variable',serif}
.priceblock span{color:var(--slate);font-size:.9rem}
.stock{color:var(--petrol-deep);font-weight:600;font-size:.92rem;margin:.3em 0 1.1em}
.btn.big{font-size:1.02rem;padding:.85em 1.8em}
.desc{margin-top:34px;border-top:1px solid var(--line);padding-top:24px}
.desc p{color:var(--graphit);white-space:pre-line}
.rel{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:20px;margin-top:18px}
.relcard{background:var(--weiss);border:1px solid var(--line);border-radius:12px;overflow:hidden}
.relcard img{width:100%;aspect-ratio:4/3;object-fit:cover}
.relcard div{padding:12px 14px;display:flex;flex-direction:column;gap:2px}
.relcard .p{color:var(--petrol);font-weight:700}
@media(max-width:780px){.detail{grid-template-columns:1fr;gap:24px}}
</style>
</Base>
+32
View File
@@ -0,0 +1,32 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Warenkorb">
<div class="wrap" style="padding-top:30px;min-height:50vh">
<h1>Dein Warenkorb</h1>
<div id="page-cart" class="page-cart"><p class="empty">Wird geladen …</p></div>
</div>
<style>
.page-cart{margin-top:20px;max-width:760px}
.pci{display:flex;gap:16px;align-items:center;padding:16px 0;border-bottom:1px solid var(--line)}
.pci img{width:80px;height:80px;border-radius:10px;object-fit:cover}
.pci .nm{font-weight:600}
.pci .q{display:flex;gap:8px;align-items:center;margin-top:6px}
.pci .q button{width:28px;height:28px;border:1px solid var(--line);background:var(--light);border-radius:7px;cursor:pointer}
.psum{margin-top:24px;text-align:right}
.psum .t{font-size:1.5rem;font-family:'Fraunces Variable',serif;font-weight:600}
.empty{color:var(--slate);padding:30px 0}
</style>
<script is:inline>
const eur=n=>new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR'}).format(n);
function paint(){
const c=window.HK?window.HK.get():[];const el=document.getElementById('page-cart');
if(!c.length){el.innerHTML='<p class="empty">Dein Warenkorb ist leer. <a href="/#shop" style="color:var(--petrol)">Weiter einkaufen →</a></p>';return}
const t=window.HK.totals();
el.innerHTML=c.map(i=>`<div class="pci"><img src="${i.bild_url}" alt=""><div style="flex:1"><div class="nm">${i.name}</div><div style="color:var(--slate);font-size:.85rem">${eur(i.preis)} · ${i.einheit||''}</div><div class="q"><button data-dec="${i.slug}"></button><span>${i.qty}</span><button data-inc="${i.slug}">+</button><button class="rm" data-rm="${i.slug}" style="margin-left:10px;background:none;border:none;color:var(--slate);text-decoration:underline;cursor:pointer">entfernen</button></div></div><strong>${eur(i.preis*i.qty)}</strong></div>`).join('')
+`<div class="psum"><div style="color:var(--graphit);font-size:.9rem">inkl. ${eur(t.tax)} MwSt.</div><div class="t">Gesamt: ${eur(t.total)}</div><button class="btn" style="margin-top:14px" onclick="alert('Demoshop — keine echte Bestellung.')">Zur Kasse (Demo)</button></div>`;
}
document.addEventListener('cart:changed',paint);
setTimeout(paint,60);
</script>
</Base>
+13
View File
@@ -0,0 +1,13 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="Widerrufsrecht">
<article class="wrap legal">
<span class="demo-note">⚠ Demo-Inhalt · Platzhaltertexte ohne Rechtswirkung</span>
<h1>Widerrufsbelehrung</h1>
<p>Verbraucher haben ein vierzehntägiges Widerrufsrecht.</p>
<h3>Widerrufsrecht</h3><p>Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Frist beginnt mit dem Tag, an dem Sie die Waren in Besitz genommen haben.</p>
<h3>Folgen des Widerrufs</h3><p>Im Falle eines wirksamen Widerrufs werden bereits erhaltene Zahlungen unverzüglich zurückerstattet.</p>
<p class="src">Hinweis: Bei schnell verderblichen Lebensmitteln kann das Widerrufsrecht gesetzlich ausgeschlossen sein. Platzhaltertext.</p>
</article>
</Base>