feat(legal): readable legal pages and corrected legal texts
Legal pages rendered admin text as bare paragraphs. LegalPage now has a honeycomb header, a sticky table of contents with scroll tracking (a collapsible one on mobile), anchor links, a reading progress bar and a revision date pulled from a "Stand:" line. lib/legal-blocks.ts derives lists, cookie/provider entries with labelled facts and safe links from plain text, so the pages stay editable in the admin without markup. Legal corrections (German law as of 2026): - Widerrufsbelehrung now follows the official model (Anlage 1 und 2 zu Art. 246a EGBGB): recipient name/address/e-mail filled from company data, deadline rule, return period, refund details, diminished value and the model withdrawal form. The incomplete notice extended the withdrawal period to up to 12 months and 14 days. Honey is no longer declared perishable. - Impressum: TMG references replaced by DDG; editorial contact gains an address. - Datenschutz: removes the self-hosted vs. hosting-provider contradiction, adds PayPal/Klarna, reviews, back-in-stock notifications, storage period, no automated decisions, withdrawal of consent and the Art. 21 objection notice; newsletter double opt-in. - AGB: payment methods and delivery area point to the live shipping page; adds contract-text and input-correction information. - Cookie policy: storage period and provider for every entry. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
86cc0ad310
commit
4ab1a7edae
9 files changed
+707
-84
No files matched your search
@@ -1,7 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LegalPage } from "@/components/legal-page";
|
import { LegalPage } from "@/components/legal-page";
|
||||||
import { CookieSettingsButton } from "@/components/cookie-consent";
|
import { CookieSettingsButton } from "@/components/cookie-consent";
|
||||||
import { Container } from "@/components/ui";
|
|
||||||
import { getContentValue, parseSections } from "@/lib/content";
|
import { getContentValue, parseSections } from "@/lib/content";
|
||||||
import { getLocale } from "@/i18n/locale";
|
import { getLocale } from "@/i18n/locale";
|
||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
@@ -13,15 +12,8 @@ export default async function CookieRichtliniePage() {
|
|||||||
const t = getDictionary(locale);
|
const t = getDictionary(locale);
|
||||||
const sections = parseSections(await getContentValue("legal.cookieRichtlinie", locale));
|
const sections = parseSections(await getContentValue("legal.cookieRichtlinie", locale));
|
||||||
return (
|
return (
|
||||||
<>
|
<LegalPage title={t.cookieRichtliniePage.title} noticeText={t.legalPage.reviewNotice} sections={sections}>
|
||||||
<LegalPage
|
<CookieSettingsButton />
|
||||||
title={t.cookieRichtliniePage.title}
|
</LegalPage>
|
||||||
noticeText={t.legalPage.reviewNotice}
|
|
||||||
sections={sections}
|
|
||||||
/>
|
|
||||||
<Container className="max-w-3xl pb-20 -mt-10">
|
|
||||||
<CookieSettingsButton />
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { LegalPage } from "@/components/legal-page";
|
import { LegalPage } from "@/components/legal-page";
|
||||||
import { getContentValue, parseSections } from "@/lib/content";
|
import { getContentValue, parseSections } from "@/lib/content";
|
||||||
|
import { getCompanyInfo } from "@/lib/company-info";
|
||||||
import { getLocale } from "@/i18n/locale";
|
import { getLocale } from "@/i18n/locale";
|
||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Widerrufsbelehrung", alternates: { canonical: "/widerrufsbelehrung" } };
|
export const metadata: Metadata = { title: "Widerrufsbelehrung", alternates: { canonical: "/widerrufsbelehrung" } };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platzhalter im Belehrungstext fuer Name, Anschrift und E-Mail des
|
||||||
|
* Empfaengers. Das amtliche Muster verlangt diese Angaben an zwei Stellen
|
||||||
|
* (Belehrung und Formular); aus den Firmendaten eingesetzt, bleiben sie bei
|
||||||
|
* einem Umzug oder neuer Adresse automatisch richtig.
|
||||||
|
*/
|
||||||
|
const PROVIDER_PLACEHOLDER = "{{anbieter}}";
|
||||||
|
|
||||||
export default async function WiderrufPage() {
|
export default async function WiderrufPage() {
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
const t = getDictionary(locale);
|
const t = getDictionary(locale);
|
||||||
const sections = parseSections(await getContentValue("legal.widerruf", locale));
|
const [company, raw] = await Promise.all([getCompanyInfo(), getContentValue("legal.widerruf", locale)]);
|
||||||
return (
|
const provider = `${company.name}, ${company.address.street}, ${company.address.zip} ${company.address.city}, E-Mail: ${company.email}`;
|
||||||
<LegalPage
|
const sections = parseSections(raw.replaceAll(PROVIDER_PLACEHOLDER, provider));
|
||||||
title={t.widerrufPage.title}
|
return <LegalPage title={t.widerrufPage.title} noticeText={t.legalPage.reviewNotice} sections={sections} />;
|
||||||
noticeText={t.legalPage.reviewNotice}
|
|
||||||
sections={sections}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
+186
-21
@@ -1,45 +1,210 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
import { Container } from "@/components/ui";
|
import { Container } from "@/components/ui";
|
||||||
|
import { HexMark, HoneycombPattern } from "@/components/honeycomb";
|
||||||
|
import { LegalToc } from "@/components/legal-toc";
|
||||||
|
import { ReadingProgress } from "@/components/reading-progress";
|
||||||
|
import { extractRevision, parseLegalParagraph, sectionAnchors, tokenizeInline, type LegalBlock } from "@/lib/legal-blocks";
|
||||||
|
import { getLocale } from "@/i18n/locale";
|
||||||
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
|
|
||||||
export interface LegalSection {
|
export interface LegalSection {
|
||||||
heading: string;
|
heading: string;
|
||||||
paragraphs: string[];
|
paragraphs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ab so vielen Abschnitten lohnt ein Inhaltsverzeichnis; darunter waere es nur Laerm. */
|
||||||
|
const MIN_SECTIONS_FOR_TOC = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gemeinsames Layout fuer alle rechtlichen Seiten (Impressum, Datenschutz,
|
* Gemeinsames Layout fuer alle rechtlichen Seiten (Impressum, Datenschutz,
|
||||||
* AGB, Widerrufsbelehrung, Versand und Zahlung). Inhalte sind Platzhalter
|
* AGB, Widerrufsbelehrung, Versand und Zahlung, Cookie-Richtlinie). Der Text
|
||||||
* und muessen vor Live-Gang durch anwaltlich geprueften Text ersetzt werden.
|
* bleibt Klartext aus dem Admin-Bereich; lib/legal-blocks.ts erkennt darin
|
||||||
|
* Aufzaehlungen, Cookie-/Anbieter-Eintraege, Links und das "Stand"-Datum.
|
||||||
*/
|
*/
|
||||||
export function LegalPage({
|
export async function LegalPage({
|
||||||
title,
|
title,
|
||||||
noticeText,
|
noticeText,
|
||||||
sections,
|
sections: rawSections,
|
||||||
|
children,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
/** Optional — leer lassen, wenn kein Hinweis (mehr) angezeigt werden soll. */
|
/** Optional — leer lassen, wenn kein Hinweis (mehr) angezeigt werden soll. */
|
||||||
noticeText: string;
|
noticeText: string;
|
||||||
sections: LegalSection[];
|
sections: LegalSection[];
|
||||||
|
/** Zusaetzliche Bedienelemente unter dem Text (z. B. Cookie-Einstellungen). */
|
||||||
|
children?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const locale = await getLocale();
|
||||||
|
const t = getDictionary(locale);
|
||||||
|
const { sections, revision } = extractRevision(rawSections);
|
||||||
|
const anchors = sectionAnchors(sections.map((s) => s.heading));
|
||||||
|
const showToc = sections.length >= MIN_SECTIONS_FOR_TOC;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-20">
|
<div className="pb-24">
|
||||||
<Container className="max-w-3xl">
|
<ReadingProgress />
|
||||||
<h1 className="text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] text-heading">{title}</h1>
|
<header className="relative overflow-hidden border-b border-ink/10 bg-surface/60">
|
||||||
{noticeText ? (
|
<HoneycombPattern className="text-honey/[0.16]" size={28} fade="right" />
|
||||||
<p className="mt-2 text-xs text-honey-dark tracking-wide-caps">{noticeText}</p>
|
<Container className="relative py-16 md:py-24">
|
||||||
) : null}
|
{/* Lange Einzelwoerter ("Widerrufsbelehrung", "Datenschutzerklärung")
|
||||||
<div className="mt-10 space-y-10">
|
passen auf 390 px nicht in 3rem — kleiner starten und notfalls
|
||||||
{sections.map((section) => (
|
an Silbengrenzen trennen statt abzuschneiden. */}
|
||||||
<section key={section.heading}>
|
<h1 lang={locale} className="max-w-3xl break-words text-[2.5rem] leading-[1.02] tracking-[-0.02em] text-heading [hyphens:auto] sm:text-5xl md:text-7xl md:leading-[0.98]">
|
||||||
<h2 className="text-2xl text-heading">{section.heading}</h2>
|
{title}
|
||||||
<div className="mt-3 space-y-3 text-sm text-ink/70 leading-relaxed">
|
</h1>
|
||||||
{section.paragraphs.map((p, i) => (
|
{revision || noticeText ? (
|
||||||
<p key={i}>{p}</p>
|
<div className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-ink/70">
|
||||||
))}
|
{revision ? (
|
||||||
</div>
|
<p className="flex items-center gap-2">
|
||||||
</section>
|
<HexMark className="h-2.5 w-2.5 text-honey" />
|
||||||
))}
|
<span>
|
||||||
|
{t.legalPage.revisionLabel}: <span className="tabular-nums text-heading">{revision}</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{noticeText ? <p className="text-honey-dark">{noticeText}</p> : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Container>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<Container className="pt-12 md:pt-16">
|
||||||
|
<div className={showToc ? "lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-20" : undefined}>
|
||||||
|
{showToc ? (
|
||||||
|
<LegalToc label={t.legalPage.tocLabel} items={sections.map((s, i) => ({ id: anchors[i], label: s.heading }))} />
|
||||||
|
) : null}
|
||||||
|
<div className="max-w-[68ch]">
|
||||||
|
{sections.map((section, i) => (
|
||||||
|
<section
|
||||||
|
key={anchors[i]}
|
||||||
|
id={anchors[i]}
|
||||||
|
aria-labelledby={`${anchors[i]}-heading`}
|
||||||
|
className="scroll-mt-28 border-t border-ink/10 pt-10 first:border-t-0 first:pt-0 [&+&]:mt-12"
|
||||||
|
>
|
||||||
|
<SectionHeading id={`${anchors[i]}-heading`} text={section.heading} />
|
||||||
|
<div className="mt-5 space-y-4 text-[0.975rem] leading-[1.75] text-ink/80">
|
||||||
|
<Blocks blocks={section.paragraphs.map(parseLegalParagraph)} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
{children ? <div className="mt-14 border-t border-ink/10 pt-10">{children}</div> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** "3. Server-Logfiles" → die Nummer leise in Honig, der Titel in der Serife. */
|
||||||
|
function SectionHeading({ id, text }: { id: string; text: string }) {
|
||||||
|
const numbered = /^(\d+)\.\s+(.+)$/.exec(text);
|
||||||
|
return (
|
||||||
|
<h2 id={id} className="text-3xl leading-tight text-heading md:text-[2.1rem]">
|
||||||
|
{numbered ? (
|
||||||
|
<>
|
||||||
|
<span className="mr-3 align-[0.35em] font-sans text-sm tabular-nums tracking-normal text-honey-dark">{numbered[1].padStart(2, "0")}</span>
|
||||||
|
{numbered[2]}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aufeinanderfolgende Eintraege bilden eine gemeinsame Tabelle mit Trennlinien. */
|
||||||
|
function Blocks({ blocks }: { blocks: LegalBlock[] }) {
|
||||||
|
const groups: (LegalBlock | LegalBlock[])[] = [];
|
||||||
|
for (const block of blocks) {
|
||||||
|
const previous = groups[groups.length - 1];
|
||||||
|
if (block.kind === "entry" && Array.isArray(previous)) previous.push(block);
|
||||||
|
else groups.push(block.kind === "entry" ? [block] : block);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map((group, i) => {
|
||||||
|
if (Array.isArray(group)) {
|
||||||
|
return (
|
||||||
|
<div key={i} className="!mt-7 divide-y divide-ink/10 border-y border-ink/10 [&+*]:!mt-7">
|
||||||
|
{group.map((entry, j) => (entry.kind === "entry" ? <Entry key={j} entry={entry} /> : null))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (group.kind === "list") {
|
||||||
|
return (
|
||||||
|
<ul key={i} className="space-y-2">
|
||||||
|
{group.items.map((item, j) => (
|
||||||
|
<li key={j} className="flex gap-3">
|
||||||
|
<HexMark className="mt-[0.55rem] h-2 w-2 flex-shrink-0 text-honey" />
|
||||||
|
<span>
|
||||||
|
<Inline text={item} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (group.kind === "text") {
|
||||||
|
return (
|
||||||
|
<p key={i} className="whitespace-pre-line">
|
||||||
|
<Inline text={group.text} />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Entry({ entry }: { entry: Extract<LegalBlock, { kind: "entry" }> }) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-x-8 gap-y-2 py-5 sm:grid-cols-[11.5rem_minmax(0,1fr)]">
|
||||||
|
<div>
|
||||||
|
{entry.note ? (
|
||||||
|
<>
|
||||||
|
<p className="font-serif text-xl leading-tight text-heading">{entry.name}</p>
|
||||||
|
<p className="mt-1 text-xs text-ink/60">{entry.note}</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// Technischer Cookie-/Speichername: Monoschrift, weil es ein
|
||||||
|
// wortwoertlich zu findender Bezeichner ist (Browser-Entwicklertools).
|
||||||
|
<code className="block break-all pt-[0.3rem] font-mono text-[0.78rem] leading-5 text-heading">{entry.name}</code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{entry.text ? (
|
||||||
|
<p>
|
||||||
|
<Inline text={entry.text} />
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{entry.facts.length > 0 ? (
|
||||||
|
<dl className="mt-3 grid gap-x-5 gap-y-1.5 text-sm sm:grid-cols-[max-content_minmax(0,1fr)]">
|
||||||
|
{entry.facts.map((fact) => (
|
||||||
|
<div key={fact.label} className="contents">
|
||||||
|
<dt className="pt-[0.2rem] text-[0.68rem] tracking-wide-caps text-honey-dark">{fact.label}</dt>
|
||||||
|
<dd className="mb-1.5 text-ink/80 sm:mb-0">
|
||||||
|
<Inline text={fact.value} />
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Inline({ text }: { text: string }) {
|
||||||
|
return tokenizeInline(text).map((token, i) =>
|
||||||
|
token.kind === "link" ? (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={token.href}
|
||||||
|
{...(token.href.startsWith("http") ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
||||||
|
className="text-heading underline decoration-honey/60 decoration-1 underline-offset-[0.22em] transition-colors hover:decoration-honey"
|
||||||
|
>
|
||||||
|
{token.value}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span key={i}>{token.value}</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
|
import { HexMark } from "@/components/honeycomb";
|
||||||
|
|
||||||
|
export interface LegalTocItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inhaltsverzeichnis der Rechtsseiten. Breit: klebt neben dem Text und
|
||||||
|
* markiert den Abschnitt, in dem man gerade liest — das Sechseck gleitet
|
||||||
|
* mit (die eine bewusste Bewegung der Seite). Schmal: aufklappbar ueber dem
|
||||||
|
* Text, damit er nicht erst nach einer langen Linkliste beginnt.
|
||||||
|
*/
|
||||||
|
export function LegalToc({ items, label }: { items: LegalTocItem[]; label: string }) {
|
||||||
|
const [activeId, setActiveId] = useState(items[0]?.id ?? "");
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const targets = items.map((item) => document.getElementById(item.id)).filter((el): el is HTMLElement => el !== null);
|
||||||
|
// Das Band liegt im oberen Drittel: aktiv ist der Abschnitt, dessen
|
||||||
|
// Ueberschrift man gerade liest, nicht der, der unten hereinragt.
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visible = entries.filter((e) => e.isIntersecting).sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
|
||||||
|
if (visible[0]) setActiveId(visible[0].target.id);
|
||||||
|
},
|
||||||
|
{ rootMargin: "-15% 0px -70% 0px" }
|
||||||
|
);
|
||||||
|
targets.forEach((el) => observer.observe(el));
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const links = (onDesktop: boolean) => (
|
||||||
|
<ol className="space-y-0.5">
|
||||||
|
{items.map((item) => {
|
||||||
|
const isActive = onDesktop && item.id === activeId;
|
||||||
|
return (
|
||||||
|
<li key={item.id}>
|
||||||
|
<a
|
||||||
|
href={`#${item.id}`}
|
||||||
|
aria-current={isActive ? "location" : undefined}
|
||||||
|
className={`group relative flex items-start gap-3 py-1.5 text-sm leading-snug transition-colors duration-300 ${
|
||||||
|
isActive ? "text-heading" : "text-ink/60 hover:text-heading"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="relative mt-[0.3rem] h-2.5 w-2.5 flex-shrink-0">
|
||||||
|
<HexMark className="absolute inset-0 h-2.5 w-2.5 text-ink/15" filled={false} />
|
||||||
|
{isActive ? (
|
||||||
|
<motion.span
|
||||||
|
layoutId="legal-toc-marker"
|
||||||
|
transition={reduceMotion ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 36 }}
|
||||||
|
className="absolute inset-0 text-honey"
|
||||||
|
>
|
||||||
|
<HexMark className="h-2.5 w-2.5" />
|
||||||
|
</motion.span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<details className="group/toc mb-12 border-y border-ink/10 lg:hidden">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center justify-between py-4 text-sm tracking-wide-caps text-heading [&::-webkit-details-marker]:hidden">
|
||||||
|
{label}
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 24 24" className="h-4 w-4 transition-transform duration-300 group-open/toc:rotate-180" fill="none" stroke="currentColor" strokeWidth="1.6">
|
||||||
|
<path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<nav aria-label={label} className="pb-5">
|
||||||
|
{links(false)}
|
||||||
|
</nav>
|
||||||
|
</details>
|
||||||
|
<nav aria-label={label} className="sticky top-28 hidden max-h-[calc(100vh-9rem)] self-start overflow-y-auto pr-2 lg:block">
|
||||||
|
<p className="mb-4 text-xs tracking-wide-caps text-ink/60">{label}</p>
|
||||||
|
{links(true)}
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+69
-21
@@ -170,6 +170,8 @@ export const de: Dictionary = {
|
|||||||
},
|
},
|
||||||
legalPage: {
|
legalPage: {
|
||||||
reviewNotice: "",
|
reviewNotice: "",
|
||||||
|
tocLabel: "Inhalt",
|
||||||
|
revisionLabel: "Stand",
|
||||||
},
|
},
|
||||||
notFoundPage: {
|
notFoundPage: {
|
||||||
title: "Diese Seite gibt es nicht, zumindest nicht mehr.",
|
title: "Diese Seite gibt es nicht, zumindest nicht mehr.",
|
||||||
@@ -200,7 +202,7 @@ export const de: Dictionary = {
|
|||||||
emailLabel: "E-Mail",
|
emailLabel: "E-Mail",
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
heading: "Angaben gemäß § 5 TMG",
|
heading: "Angaben gemäß § 5 DDG",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Inhaber: Nick Rosenlöcher",
|
"Inhaber: Nick Rosenlöcher",
|
||||||
"Rechtsform: Einzelunternehmen",
|
"Rechtsform: Einzelunternehmen",
|
||||||
@@ -215,13 +217,13 @@ export const de: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "Redaktionell verantwortlich",
|
heading: "Redaktionell verantwortlich",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Verantwortlich für den redaktionellen Inhalt gemäß § 18 Abs. 2 MStV (u.a. die Beiträge im Bereich \"Honigwissen\"): Nick Rosenlöcher.",
|
"Verantwortlich für den redaktionellen Inhalt gemäß § 18 Abs. 2 MStV (u.a. die Beiträge im Bereich \"Honigwissen\"): Nick Rosenlöcher, Anschrift wie oben.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Haftung für Inhalte",
|
heading: "Haftung für Inhalte",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Als Diensteanbieter sind wir gemäß § 7 Abs. 1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden entsprechender Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.",
|
"Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden entsprechender Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -255,7 +257,7 @@ export const de: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "1. Verantwortlicher",
|
heading: "1. Verantwortlicher",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Nach unserer Einschätzung sind wir aufgrund der Größe und Art unseres Betriebs nicht gemäß § 38 BDSG zur Benennung einer/eines Datenschutzbeauftragten verpflichtet. Bei Fragen zum Datenschutz wenden Sie sich bitte direkt an die im Impressum genannte Kontaktadresse.",
|
"Nach unserer Einschätzung sind wir aufgrund der Größe und Art unseres Betriebs nicht gemäß § 38 BDSG zur Benennung einer/eines Datenschutzbeauftragten verpflichtet. Bei Fragen zum Datenschutz wende dich bitte direkt an die oben genannte Kontaktadresse.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -267,7 +269,7 @@ export const de: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "3. Server-Logfiles beim Besuch der Website",
|
heading: "3. Server-Logfiles beim Besuch der Website",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Beim Aufruf unserer Website erhebt unser Hosting-Anbieter automatisch technische Zugriffsdaten (z.B. IP-Adresse, Datum und Uhrzeit des Zugriffs, aufgerufene Seite, Browsertyp, Referrer-URL) in sogenannten Server-Logfiles. Diese Verarbeitung ist zur Bereitstellung der Website technisch erforderlich (Art. 6 Abs. 1 lit. f DSGVO) und wird nach kurzer Zeit automatisch gelöscht.",
|
"Beim Aufruf unserer Website erheben wir auf unserem Server automatisch technische Zugriffsdaten (z.B. IP-Adresse, Datum und Uhrzeit des Zugriffs, aufgerufene Seite, Browsertyp, Referrer-URL) in sogenannten Server-Logfiles. Diese Verarbeitung ist zur Bereitstellung der Website technisch erforderlich (Art. 6 Abs. 1 lit. f DSGVO) Die Logfiles werden nicht mit anderen Daten zusammengeführt und automatisch gelöscht, sobald sie für die Sicherheit und Fehleranalyse nicht mehr benötigt werden.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,14 +278,15 @@ export const de: Dictionary = {
|
|||||||
"Für den Versand übermitteln wir deinen Namen und die Lieferanschrift an unseren Versanddienstleister DHL (DHL Paket GmbH, Bonn). Wählst du die Lieferung an eine Packstation oder Filiale, kommt die von dir gewählte Standortnummer hinzu. Suchst du im Bestellvorgang nach einer Packstation, wird dein eingegebener Ort beziehungsweise deine Postleitzahl zur Abfrage an DHL weitergegeben. Rufst du den Stand deiner Sendung unter „Bestellung verfolgen“ ab, fragen wir ihn mit Sendungsnummer und Postleitzahl der Lieferanschrift bei DHL ab und zeigen ihn dir direkt bei uns an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.",
|
"Für den Versand übermitteln wir deinen Namen und die Lieferanschrift an unseren Versanddienstleister DHL (DHL Paket GmbH, Bonn). Wählst du die Lieferung an eine Packstation oder Filiale, kommt die von dir gewählte Standortnummer hinzu. Suchst du im Bestellvorgang nach einer Packstation, wird dein eingegebener Ort beziehungsweise deine Postleitzahl zur Abfrage an DHL weitergegeben. Rufst du den Stand deiner Sendung unter „Bestellung verfolgen“ ab, fragen wir ihn mit Sendungsnummer und Postleitzahl der Lieferanschrift bei DHL ab und zeigen ihn dir direkt bei uns an. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.",
|
||||||
"Bei einer Bestellung erheben wir die für die Vertrags-, Zahlungs- und Versandabwicklung notwendigen Daten (Name, Adresse, E-Mail, ggf. Telefonnummer sowie die bestellten Artikel). Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung).",
|
"Bei einer Bestellung erheben wir die für die Vertrags-, Zahlungs- und Versandabwicklung notwendigen Daten (Name, Adresse, E-Mail, ggf. Telefonnummer sowie die bestellten Artikel). Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung).",
|
||||||
"Zahlungen werden über unseren Zahlungsdienstleister Stripe Payments Europe, Ltd. abgewickelt. Dabei werden die für die Zahlungsabwicklung notwendigen Daten an Stripe übermittelt. Es gelten zusätzlich die Datenschutzhinweise von Stripe (stripe.com/de/privacy). Soweit dabei Daten an mit Stripe verbundene Unternehmen außerhalb der EU/des EWR (insbesondere Stripe, Inc. in den USA) übermittelt werden, erfolgt dies auf Grundlage von EU-Standardvertragsklauseln gemäß Art. 46 DSGVO.",
|
"Zahlungen werden über unseren Zahlungsdienstleister Stripe Payments Europe, Ltd. abgewickelt. Dabei werden die für die Zahlungsabwicklung notwendigen Daten an Stripe übermittelt. Es gelten zusätzlich die Datenschutzhinweise von Stripe (stripe.com/de/privacy). Soweit dabei Daten an mit Stripe verbundene Unternehmen außerhalb der EU/des EWR (insbesondere Stripe, Inc. in den USA) übermittelt werden, erfolgt dies auf Grundlage von EU-Standardvertragsklauseln gemäß Art. 46 DSGVO.",
|
||||||
|
"Wählst du im Bezahlschritt PayPal oder Klarna, wirst du zum jeweiligen Anbieter weitergeleitet, und die für die Zahlung erforderlichen Bestelldaten werden an ihn übermittelt: PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449 Luxemburg, bzw. Klarna Bank AB (publ), Sveavägen 46, 111 34 Stockholm, Schweden. Diese Anbieter können eigene Prüfungen durchführen, etwa zur Identität oder Zahlungsfähigkeit; dafür sind sie selbst verantwortlich, und es gelten ihre Datenschutzhinweise. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.",
|
||||||
"Bestell- und Rechnungsdaten werden aufgrund handels- und steuerrechtlicher Aufbewahrungspflichten (§ 257 HGB, § 147 AO) für die gesetzlich vorgeschriebene Dauer gespeichert.",
|
"Bestell- und Rechnungsdaten werden aufgrund handels- und steuerrechtlicher Aufbewahrungspflichten (§ 257 HGB, § 147 AO) für die gesetzlich vorgeschriebene Dauer gespeichert.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "5. Kundenkonto",
|
heading: "5. Kundenkonto",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Alternativ kannst du dich mit einem Microsoft-Konto anmelden (Microsoft Entra ID). Dabei wirst du zu Microsoft weitergeleitet und meldest dich dort an; wir selbst erhalten kein Passwort, sondern nur deine E-Mail-Adresse und deinen Namen, um dein Kundenkonto zuzuordnen. Welche Daten Microsoft bei der Anmeldung selbst verarbeitet, richtet sich nach den Datenschutzhinweisen von Microsoft. Anbieter ist die Microsoft Ireland Operations Limited; eine Übermittlung an die Microsoft Corporation in die USA kann stattfinden. Microsoft ist unter dem EU-US Data Privacy Framework zertifiziert. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.",
|
|
||||||
"Legst du ein Kundenkonto an, speichern wir die von dir angegebenen Daten (Name, E-Mail, Passwort verschlüsselt als Hash) sowie optional hinterlegte Lieferadressen, um dir Bestellhistorie, Rechnungseinsicht und eine schnellere Kasse zu ermöglichen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO. Du kannst dein Konto und die zugehörigen Daten jederzeit über eine formlose Anfrage an uns löschen lassen.",
|
"Legst du ein Kundenkonto an, speichern wir die von dir angegebenen Daten (Name, E-Mail, Passwort verschlüsselt als Hash) sowie optional hinterlegte Lieferadressen, um dir Bestellhistorie, Rechnungseinsicht und eine schnellere Kasse zu ermöglichen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO. Du kannst dein Konto und die zugehörigen Daten jederzeit über eine formlose Anfrage an uns löschen lassen.",
|
||||||
|
"Alternativ kannst du dich mit einem Microsoft-Konto anmelden (Microsoft Entra ID). Dabei wirst du zu Microsoft weitergeleitet und meldest dich dort an; wir selbst erhalten kein Passwort, sondern nur deine E-Mail-Adresse und deinen Namen, um dein Kundenkonto zuzuordnen. Welche Daten Microsoft bei der Anmeldung selbst verarbeitet, richtet sich nach den Datenschutzhinweisen von Microsoft. Anbieter ist die Microsoft Ireland Operations Limited; eine Übermittlung an die Microsoft Corporation in die USA kann stattfinden. Microsoft ist unter dem EU-US Data Privacy Framework zertifiziert. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.",
|
||||||
"Zur Anmeldung wird ein technisch notwendiges Cookie gesetzt, siehe unsere Cookie-Richtlinie.",
|
"Zur Anmeldung wird ein technisch notwendiges Cookie gesetzt, siehe unsere Cookie-Richtlinie.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -291,6 +294,7 @@ export const de: Dictionary = {
|
|||||||
heading: "6. Newsletter (Erntepost)",
|
heading: "6. Newsletter (Erntepost)",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Meldest du dich zu unserer Erntepost an, verwenden wir deine E-Mail-Adresse ausschließlich zum Versand von Informationen über neue Ernten. Rechtsgrundlage ist deine Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Eine Abmeldung ist jederzeit über den Link in jeder E-Mail oder im Kundenkonto möglich.",
|
"Meldest du dich zu unserer Erntepost an, verwenden wir deine E-Mail-Adresse ausschließlich zum Versand von Informationen über neue Ernten. Rechtsgrundlage ist deine Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Eine Abmeldung ist jederzeit über den Link in jeder E-Mail oder im Kundenkonto möglich.",
|
||||||
|
"Die Anmeldung erfolgt im Double-Opt-in-Verfahren: Eingetragen wirst du erst, nachdem du den Link in unserer Bestätigungs-E-Mail angeklickt hast. Mit der Abmeldung widerrufst du deine Einwilligung mit Wirkung für die Zukunft (Art. 7 Abs. 3 DSGVO). Deine Adresse bewahren wir danach mit dem Abmeldezeitpunkt auf, um die Einwilligung und ihren Widerruf nachweisen zu können und dir keine weitere Erntepost zu schicken (Art. 6 Abs. 1 lit. f DSGVO).",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -315,14 +319,36 @@ export const de: Dictionary = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "10. Cookies",
|
heading: "10. Produktbewertungen",
|
||||||
|
paragraphs: [
|
||||||
|
"Schreibst du als angemeldete Kundin oder angemeldeter Kunde eine Bewertung, speichern wir Sternebewertung, Text, Zeitpunkt und die Zuordnung zu deinem Kundenkonto. Nach unserer Freigabe erscheint die Bewertung auf der Produktseite zusammen mit deinem Vornamen und dem Datum. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO; die Bewertung bleibt gespeichert, bis du ihre Löschung verlangst oder dein Kundenkonto gelöscht wird.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "11. Benachrichtigung bei Verfügbarkeit",
|
||||||
|
paragraphs: [
|
||||||
|
"Lässt du dich über die Verfügbarkeit eines ausverkauften Produkts informieren, speichern wir deine E-Mail-Adresse, das Produkt und deine Sprache, um dir genau eine Benachrichtigung zu schicken, sobald es wieder erhältlich ist. Rechtsgrundlage ist deine Anfrage (Art. 6 Abs. 1 lit. b DSGVO). Du kannst die Anmeldung jederzeit formlos bei uns löschen lassen.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "12. Cookies",
|
||||||
paragraphs: ["Details zu den auf dieser Website eingesetzten Cookies findest du in unserer Cookie-Richtlinie."],
|
paragraphs: ["Details zu den auf dieser Website eingesetzten Cookies findest du in unserer Cookie-Richtlinie."],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "11. Deine Rechte",
|
heading: "13. Speicherdauer und Pflicht zur Bereitstellung",
|
||||||
|
paragraphs: [
|
||||||
|
"Wir speichern personenbezogene Daten nur so lange, wie es für den jeweiligen Zweck erforderlich ist oder gesetzliche Aufbewahrungsfristen es verlangen (bei Bestell- und Rechnungsdaten bis zu zehn Jahre nach § 147 AO, § 257 HGB). Danach werden sie gelöscht.",
|
||||||
|
"Die Angaben, die für eine Bestellung erforderlich sind, brauchen wir, um den Vertrag zu schließen und zu erfüllen; ohne sie ist keine Bestellung möglich. Alle weiteren Angaben (z. B. Newsletter, Bewertungen) sind freiwillig.",
|
||||||
|
"Eine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "14. Deine Rechte",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Du hast das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO), Datenübertragbarkeit (Art. 20 DSGVO) sowie Widerspruch gegen die Verarbeitung (Art. 21 DSGVO) deiner personenbezogenen Daten.",
|
"Du hast das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO), Datenübertragbarkeit (Art. 20 DSGVO) sowie Widerspruch gegen die Verarbeitung (Art. 21 DSGVO) deiner personenbezogenen Daten.",
|
||||||
"Zudem steht dir ein Beschwerderecht bei einer Datenschutz-Aufsichtsbehörde zu, z.B. dem Bayerischen Landesamt für Datenschutzaufsicht.",
|
"Zudem steht dir ein Beschwerderecht bei einer Datenschutz-Aufsichtsbehörde zu, z.B. dem Bayerischen Landesamt für Datenschutzaufsicht (BayLDA), Promenade 18, 91522 Ansbach, www.lda.bayern.de.",
|
||||||
|
"Hast du eine Einwilligung erteilt, kannst du sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3 DSGVO). Die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt davon unberührt.",
|
||||||
|
"Widerspruchsrecht: Verarbeiten wir Daten auf Grundlage berechtigter Interessen (Art. 6 Abs. 1 lit. f DSGVO), kannst du aus Gründen, die sich aus deiner besonderen Situation ergeben, jederzeit widersprechen (Art. 21 Abs. 1 DSGVO). Wir verarbeiten die Daten dann nicht mehr, es sei denn, wir können zwingende schutzwürdige Gründe nachweisen. Einer Verarbeitung zu Zwecken der Direktwerbung kannst du jederzeit ohne Angabe von Gründen widersprechen (Art. 21 Abs. 2 DSGVO). Eine formlose Nachricht an uns genügt.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -340,6 +366,7 @@ export const de: Dictionary = {
|
|||||||
heading: "2. Vertragsschluss",
|
heading: "2. Vertragsschluss",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Die Darstellung der Produkte im Shop stellt kein bindendes Angebot dar, sondern eine unverbindliche Aufforderung, eine Bestellung aufzugeben. Mit Abschluss der Bestellung gibst du ein verbindliches Angebot zum Kauf ab, das wir durch eine Bestätigung per E-Mail oder durch Lieferung der Ware annehmen.",
|
"Die Darstellung der Produkte im Shop stellt kein bindendes Angebot dar, sondern eine unverbindliche Aufforderung, eine Bestellung aufzugeben. Mit Abschluss der Bestellung gibst du ein verbindliches Angebot zum Kauf ab, das wir durch eine Bestätigung per E-Mail oder durch Lieferung der Ware annehmen.",
|
||||||
|
"Bis zum Abschluss der Zahlung kannst du deine Eingaben im Bestellformular jederzeit korrigieren; aus dem Zahlungsschritt führt „Zurück zu deinen Angaben“ wieder zum Formular. Der Vertrag wird in deutscher Sprache geschlossen. Wir speichern den Vertragstext; die Bestelldaten erhältst du mit der Bestätigung per E-Mail, und mit Kundenkonto kannst du deine Bestellungen dort jederzeit einsehen. Diese AGB kannst du auf dieser Seite jederzeit abrufen, ausdrucken und speichern.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -349,18 +376,18 @@ export const de: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "4. Zahlungsarten",
|
heading: "4. Zahlungsarten",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Wir bieten PayPal, Kreditkarte, Apple Pay und Google Pay über unseren Zahlungsdienstleister Stripe an. Eine Zahlung per Überweisung ist auf Anfrage möglich. Details siehe unsere Seite \"Versand und Zahlung\" sowie unsere Datenschutzerklärung zu Stripe.",
|
"Die Zahlung erfolgt über unseren Zahlungsdienstleister Stripe. Welche Zahlungsarten (z. B. Kreditkarte, SEPA-Lastschrift, Klarna, PayPal, Apple Pay, Google Pay) zur Verfügung stehen, zeigen wir im Bestellvorgang und auf unserer Seite \"Versand und Zahlung\" an. Eine Zahlung per Überweisung ist auf Anfrage möglich.",
|
||||||
"Die Kaufpreiszahlung ist grundsätzlich sofort mit Bestellung fällig (Vorkasse über den gewählten Zahlungsdienst), sofern nichts anderes vereinbart wurde.",
|
"Die Kaufpreiszahlung ist grundsätzlich sofort mit Bestellung fällig (Vorkasse über den gewählten Zahlungsdienst), sofern nichts anderes vereinbart wurde.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "5. Lieferung und Abholung",
|
heading: "5. Lieferung und Abholung",
|
||||||
paragraphs: ["Wir liefern innerhalb Deutschlands. Alternativ kann die Ware nach Absprache direkt bei uns in Altenmünster abgeholt werden."],
|
paragraphs: ["Wir liefern in das auf unserer Seite \"Versand und Zahlung\" genannte Liefergebiet. Alternativ kann die Ware nach Absprache direkt bei uns in Altenmünster abgeholt werden."],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "6. Widerrufsrecht",
|
heading: "6. Widerrufsrecht",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Verbraucher:innen steht grundsätzlich ein gesetzliches Widerrufsrecht zu. Einzelheiten, insbesondere zu Ausnahmen bei schnell verderblichen Waren wie Honig, entnimmst du unserer separaten Widerrufsbelehrung.",
|
"Verbraucher:innen steht grundsätzlich ein gesetzliches Widerrufsrecht zu. Einzelheiten, insbesondere zu den gesetzlichen Ausnahmen, entnimmst du unserer separaten Widerrufsbelehrung.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -396,16 +423,27 @@ export const de: Dictionary = {
|
|||||||
title: "Widerrufsbelehrung",
|
title: "Widerrufsbelehrung",
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
|
// Gesamte Belehrung = amtliches Muster (Anlage 1 zu Art. 246a § 1
|
||||||
|
// Abs. 2 Satz 2 EGBGB), nur in der Du-Form. Nur das vollstaendige
|
||||||
|
// Muster loest die Gesetzlichkeitsfiktion aus; fehlt z. B. die
|
||||||
|
// Anschrift, verlaengert sich die Widerrufsfrist auf bis zu zwoelf
|
||||||
|
// Monate und 14 Tage (§ 356 Abs. 3 BGB). {{anbieter}} ersetzt
|
||||||
|
// app/widerrufsbelehrung/page.tsx durch Name, Anschrift und E-Mail aus
|
||||||
|
// den Firmendaten.
|
||||||
heading: "Widerrufsrecht",
|
heading: "Widerrufsrecht",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Du hast das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem du oder ein von dir benannter Dritter die Waren in Besitz genommen hat.",
|
"Du hast das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.",
|
||||||
"Um dein Widerrufsrecht auszuüben, musst du uns mittels einer eindeutigen Erklärung (z.B. per E-Mail) über deinen Entschluss, diesen Vertrag zu widerrufen, informieren.",
|
"Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem du oder ein von dir benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen hast bzw. hat.",
|
||||||
|
"Um dein Widerrufsrecht auszuüben, musst du uns ({{anbieter}}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über deinen Entschluss, diesen Vertrag zu widerrufen, informieren. Du kannst dafür das unten stehende Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.",
|
||||||
|
"Zur Wahrung der Widerrufsfrist reicht es aus, dass du die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absendest.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Folgen des Widerrufs",
|
heading: "Folgen des Widerrufs",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Wenn du diesen Vertrag widerrufst, erstatten wir dir alle Zahlungen, die wir von dir erhalten haben, unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurück, an dem die Mitteilung über deinen Widerruf bei uns eingegangen ist.",
|
"Wenn du diesen Vertrag widerrufst, haben wir dir alle Zahlungen, die wir von dir erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass du eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt hast), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über deinen Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das du bei der ursprünglichen Transaktion eingesetzt hast, es sei denn, mit dir wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden dir wegen dieser Rückzahlung Entgelte berechnet.",
|
||||||
|
"Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis du den Nachweis erbracht hast, dass du die Waren zurückgesandt hast, je nachdem, welches der frühere Zeitpunkt ist.",
|
||||||
|
"Du hast die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem du uns über den Widerruf dieses Vertrags unterrichtest, an uns zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn du die Waren vor Ablauf der Frist von vierzehn Tagen absendest.",
|
||||||
// Wortlaut aus der amtlichen Muster-Widerrufsbelehrung (Anlage 1 zu
|
// Wortlaut aus der amtlichen Muster-Widerrufsbelehrung (Anlage 1 zu
|
||||||
// Art. 246a § 1 Abs. 2 EGBGB), bewusst unveraendert uebernommen.
|
// Art. 246a § 1 Abs. 2 EGBGB), bewusst unveraendert uebernommen.
|
||||||
// Ohne diese Information traegt nach § 357 Abs. 6 BGB der Verkaeufer
|
// Ohne diese Information traegt nach § 357 Abs. 6 BGB der Verkaeufer
|
||||||
@@ -416,12 +454,22 @@ export const de: Dictionary = {
|
|||||||
// Abweichung vom Muster kostet dessen Gesetzlichkeitsfiktion. Sie
|
// Abweichung vom Muster kostet dessen Gesetzlichkeitsfiktion. Sie
|
||||||
// steht stattdessen unter "Versand und Zahlung" → "Rücksendung".
|
// steht stattdessen unter "Versand und Zahlung" → "Rücksendung".
|
||||||
"Du trägst die unmittelbaren Kosten der Rücksendung der Waren.",
|
"Du trägst die unmittelbaren Kosten der Rücksendung der Waren.",
|
||||||
|
"Du musst für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Ausschluss des Widerrufsrechts",
|
heading: "Ausschluss des Widerrufsrechts",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Das Widerrufsrecht besteht nicht bei Lieferung von Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde, sowie bei versiegelten Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde.",
|
"Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB). Das betrifft insbesondere geöffnete Honiggläser, deren Sicherheitsverschluss aufgebrochen wurde.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Muster-Widerrufsformular",
|
||||||
|
paragraphs: [
|
||||||
|
"(Wenn du den Vertrag widerrufen willst, dann fülle bitte dieses Formular aus und sende es zurück.)",
|
||||||
|
"An: {{anbieter}}",
|
||||||
|
"- Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)\n- Bestellt am (*)/erhalten am (*)\n- Name des/der Verbraucher(s)\n- Anschrift des/der Verbraucher(s)\n- Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)\n- Datum",
|
||||||
|
"(*) Unzutreffendes streichen.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -503,17 +551,17 @@ export const de: Dictionary = {
|
|||||||
heading: "Lokaler Speicher (localStorage)",
|
heading: "Lokaler Speicher (localStorage)",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Die folgenden Einträge liegen im lokalen Speicher deines Browsers. Anders als Cookies werden sie nicht bei jedem Seitenaufruf automatisch an uns gesendet und laufen nicht von selbst ab: Sie bleiben, bis du sie löschst (über die Browser-Einstellungen „Websitedaten löschen“) oder die Website sie entfernt. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. b bzw. lit. f DSGVO.",
|
"Die folgenden Einträge liegen im lokalen Speicher deines Browsers. Anders als Cookies werden sie nicht bei jedem Seitenaufruf automatisch an uns gesendet und laufen nicht von selbst ab: Sie bleiben, bis du sie löschst (über die Browser-Einstellungen „Websitedaten löschen“) oder die Website sie entfernt. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. b bzw. lit. f DSGVO.",
|
||||||
"wabenhain-cart – Dein Warenkorb (Artikel, Mengen, Preise). An uns übermittelt wird er erst, wenn du eine Bestellung abschickst. Nach erfolgreicher Bestellung wird er geleert.",
|
"wabenhain-cart – Dein Warenkorb (Artikel, Mengen, Preise). An uns übermittelt wird er erst, wenn du eine Bestellung abschickst. Nach erfolgreicher Bestellung wird er geleert. Speicherdauer: bis zur nächsten Bestellung oder bis du ihn selbst leerst.",
|
||||||
"wabenhain-recently-viewed – Die Kennungen der bis zu 6 zuletzt angesehenen Produkte, um sie dir auf Produktseiten erneut anzuzeigen. Zum Anzeigen werden nur diese Produktkennungen abgefragt; wir speichern keinen Verlauf auf unseren Servern.",
|
"wabenhain-recently-viewed – Die Kennungen der bis zu 6 zuletzt angesehenen Produkte, um sie dir auf Produktseiten erneut anzuzeigen. Zum Anzeigen werden nur diese Produktkennungen abgefragt; wir speichern keinen Verlauf auf unseren Servern. Speicherdauer: bis du die Websitedaten in deinem Browser löschst.",
|
||||||
"wabenhain-cookie-consent – Deine Auswahl im Cookie-Banner („Nur notwendige“ oder „Alle akzeptieren“), damit der Banner nicht bei jedem Besuch erneut erscheint und wir deine Entscheidung nachweisen können. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit Art. 7 Abs. 1 DSGVO.",
|
"wabenhain-cookie-consent – Deine Auswahl im Cookie-Banner („Nur notwendige“ oder „Alle akzeptieren“), damit der Banner nicht bei jedem Besuch erneut erscheint und wir deine Entscheidung nachweisen können. Speicherdauer: bis du deine Auswahl änderst oder die Websitedaten löschst. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. c DSGVO in Verbindung mit Art. 7 Abs. 1 DSGVO.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Cookies von Drittanbietern",
|
heading: "Cookies von Drittanbietern",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Stripe (Zahlungsabwicklung): Erst im Zahlungsschritt des Bestellabschlusses lädt dein Browser das Zahlungsformular von Stripe. Stripe setzt dabei zur Betrugsprävention und für die sichere Zahlungsabwicklung eigene Cookies, insbesondere __stripe_mid (Speicherdauer 1 Jahr) und __stripe_sid (Speicherdauer 30 Minuten) sowie weitere Cookies innerhalb des Zahlungsformulars auf Domains von Stripe. Name und Laufzeit dieser Cookies legt Stripe fest. Anbieter: Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Irland. Eine Übermittlung an Stripe, Inc. in die USA kann stattfinden; sie erfolgt auf Grundlage von EU-Standardvertragsklauseln (Art. 46 DSGVO). Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. b DSGVO. Weitere Informationen: stripe.com/de/privacy und stripe.com/de/legal/cookies-policy.",
|
"Stripe (Zahlungsabwicklung): Erst im Zahlungsschritt des Bestellabschlusses lädt dein Browser das Zahlungsformular von Stripe. Stripe setzt dabei zur Betrugsprävention und für die sichere Zahlungsabwicklung eigene Cookies, insbesondere __stripe_mid (Speicherdauer 1 Jahr) und __stripe_sid (Speicherdauer 30 Minuten) sowie weitere Cookies innerhalb des Zahlungsformulars auf Domains von Stripe. Name und Laufzeit dieser Cookies legt Stripe fest. Anbieter: Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Irland. Eine Übermittlung an Stripe, Inc. in die USA kann stattfinden; sie erfolgt auf Grundlage von EU-Standardvertragsklauseln (Art. 46 DSGVO). Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. b DSGVO. Weitere Informationen: stripe.com/de/privacy und stripe.com/de/legal/cookies-policy.",
|
||||||
"Microsoft (Anmeldung mit Microsoft-Konto): Wählst du „Mit Microsoft anmelden“, wirst du auf eine Anmeldeseite von Microsoft weitergeleitet. Die dort gesetzten Cookies verantwortet Microsoft (Microsoft Ireland Operations Limited); es gelten die Datenschutzhinweise von Microsoft. Auf unserer Website setzt Microsoft keine Cookies.",
|
"Microsoft (Anmeldung mit Microsoft-Konto): Wählst du „Mit Microsoft anmelden“, wirst du auf eine Anmeldeseite von Microsoft weitergeleitet. Die dort gesetzten Cookies verantwortet Microsoft; es gelten die Datenschutzhinweise von Microsoft. Auf unserer Website setzt Microsoft keine Cookies. Anbieter: Microsoft Ireland Operations Limited, One Microsoft Place, South County Business Park, Leopardstown, Dublin 18, Irland.",
|
||||||
"Cloudflare Turnstile (Schutz der Formulare): Auf Seiten mit Formularen (Anmeldung, Registrierung, Passwort-Reset, Zwei-Faktor-Bestätigung, Bestellabschluss, Newsletter, Firmenanfrage, Verfügbarkeitsbenachrichtigung) lädt dein Browser ein Skript von challenges.cloudflare.com. Es unterscheidet Eingaben von Menschen und automatisierten Programmen. Dabei werden deine IP-Adresse sowie technische Merkmale deines Browsers an Cloudflare, Inc. (USA) übermittelt. Nach Angaben des Anbieters setzt Turnstile keine Cookies zu Werbezwecken und erstellt kein seitenübergreifendes Nutzerprofil. Cloudflare ist unter dem EU-US Data Privacy Framework zertifiziert. Einzelheiten in unserer Datenschutzerklärung.",
|
"Cloudflare Turnstile (Schutz der Formulare): Auf Seiten mit Formularen (Anmeldung, Registrierung, Passwort-Reset, Zwei-Faktor-Bestätigung, Bestellabschluss, Newsletter, Firmenanfrage, Verfügbarkeitsbenachrichtigung) lädt dein Browser ein Skript von challenges.cloudflare.com. Es unterscheidet Eingaben von Menschen und automatisierten Programmen. Dabei werden deine IP-Adresse sowie technische Merkmale deines Browsers an Cloudflare, Inc. (USA) übermittelt. Nach Angaben des Anbieters setzt Turnstile keine Cookies zu Werbezwecken und erstellt kein seitenübergreifendes Nutzerprofil. Cloudflare ist unter dem EU-US Data Privacy Framework zertifiziert. Anbieter: Cloudflare, Inc., 101 Townsend Street, San Francisco, CA 94107, USA. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. f DSGVO. Weitere Informationen: siehe Abschnitt „Cloudflare Turnstile“ in unserer Datenschutzerklärung.",
|
||||||
"Google-Rezensionen: Die Bewertungen auf unserer Startseite rufen wir serverseitig über die Google Places API ab. Dein Browser baut dabei keine Verbindung zu Google auf, und es wird kein Cookie von Google gesetzt.",
|
"Google-Rezensionen: Die Bewertungen auf unserer Startseite rufen wir serverseitig über die Google Places API ab. Dein Browser baut dabei keine Verbindung zu Google auf, und es wird kein Cookie von Google gesetzt.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+64
-23
@@ -170,6 +170,8 @@ export const en: Dictionary = {
|
|||||||
},
|
},
|
||||||
legalPage: {
|
legalPage: {
|
||||||
reviewNotice: "",
|
reviewNotice: "",
|
||||||
|
tocLabel: "Contents",
|
||||||
|
revisionLabel: "Last updated",
|
||||||
},
|
},
|
||||||
notFoundPage: {
|
notFoundPage: {
|
||||||
title: "This page doesn't exist, at least not anymore.",
|
title: "This page doesn't exist, at least not anymore.",
|
||||||
@@ -200,7 +202,7 @@ export const en: Dictionary = {
|
|||||||
emailLabel: "Email",
|
emailLabel: "Email",
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
heading: "Information pursuant to § 5 TMG (German Telemedia Act)",
|
heading: "Information pursuant to § 5 DDG (German Digital Services Act)",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Owner: Nick Rosenlöcher",
|
"Owner: Nick Rosenlöcher",
|
||||||
"Legal form: sole proprietorship (Einzelunternehmen)",
|
"Legal form: sole proprietorship (Einzelunternehmen)",
|
||||||
@@ -215,13 +217,13 @@ export const en: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "Editorially Responsible",
|
heading: "Editorially Responsible",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Responsible for editorial content pursuant to § 18(2) MStV (German Interstate Media Treaty), including the articles in our \"Honey Knowledge\" section: Nick Rosenlöcher.",
|
"Responsible for editorial content pursuant to § 18(2) MStV (German Interstate Media Treaty), including the articles in our \"Honey Knowledge\" section: Nick Rosenlöcher, address as above.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Liability for Content",
|
heading: "Liability for Content",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"As a service provider, we are responsible for our own content on these pages under the general laws pursuant to § 7(1) TMG. However, pursuant to §§ 8 to 10 TMG, we as a service provider are not obligated to monitor transmitted or stored third-party information or to investigate circumstances that indicate unlawful activity. Obligations to remove or block the use of information under general law remain unaffected. However, liability in this regard is only possible from the point in time at which we become aware of a specific infringement. Upon becoming aware of any such infringements, we will remove the relevant content immediately.",
|
"As a service provider, we are responsible for our own content on these pages under the general laws pursuant to § 7(1) DDG. However, pursuant to §§ 8 to 10 DDG, we as a service provider are not obligated to monitor transmitted or stored third-party information or to investigate circumstances that indicate unlawful activity. Obligations to remove or block the use of information under general law remain unaffected. However, liability in this regard is only possible from the point in time at which we become aware of a specific infringement. Upon becoming aware of any such infringements, we will remove the relevant content immediately.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,7 +254,7 @@ export const en: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "1. Data Controller",
|
heading: "1. Data Controller",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Based on the size and nature of our business, we do not believe we are required to appoint a data protection officer under § 38 BDSG. For any data protection questions, please use the contact address given in our legal notice.",
|
"Based on the size and nature of our business, we do not believe we are required to appoint a data protection officer under § 38 BDSG. For any data protection questions, please use the contact address given above.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -264,7 +266,7 @@ export const en: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "3. Server Log Files When Visiting the Website",
|
heading: "3. Server Log Files When Visiting the Website",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"When you access our website, our hosting provider automatically collects technical access data (e.g. IP address, date and time of access, page accessed, browser type, referrer URL) in what are known as server log files. This processing is technically necessary to provide the website (Art. 6(1)(f) GDPR) and is deleted automatically after a short time.",
|
"When you access our website, we automatically collect on our server technical access data (e.g. IP address, date and time of access, page accessed, browser type, referrer URL) in what are known as server log files. This processing is technically necessary to provide the website (Art. 6(1)(f) GDPR) The log files are not combined with other data and are deleted automatically once they are no longer needed for security and error analysis.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -273,14 +275,15 @@ export const en: Dictionary = {
|
|||||||
"For shipping, we pass your name and delivery address to our shipping provider DHL (DHL Paket GmbH, Bonn, Germany). If you choose delivery to a Packstation or branch, the location number you selected is added. If you search for a Packstation during checkout, the town or postcode you enter is sent to DHL to perform that search. When you check your shipment under “Track order”, we request its status from DHL using the tracking number and the delivery postcode and show it to you directly on our site. Legal basis: Art. 6(1)(b) GDPR.",
|
"For shipping, we pass your name and delivery address to our shipping provider DHL (DHL Paket GmbH, Bonn, Germany). If you choose delivery to a Packstation or branch, the location number you selected is added. If you search for a Packstation during checkout, the town or postcode you enter is sent to DHL to perform that search. When you check your shipment under “Track order”, we request its status from DHL using the tracking number and the delivery postcode and show it to you directly on our site. Legal basis: Art. 6(1)(b) GDPR.",
|
||||||
"When you place an order, we collect the data necessary for contract, payment, and shipping processing (name, address, email, phone number if provided, and the items ordered). The legal basis is Art. 6(1)(b) GDPR (contract performance).",
|
"When you place an order, we collect the data necessary for contract, payment, and shipping processing (name, address, email, phone number if provided, and the items ordered). The legal basis is Art. 6(1)(b) GDPR (contract performance).",
|
||||||
"Payments are processed by our payment provider, Stripe Payments Europe, Ltd. The data necessary for payment processing is transmitted to Stripe for this purpose. Stripe's privacy policy also applies (stripe.com/privacy). Where this involves transferring data to Stripe-affiliated companies outside the EU/EEA (in particular Stripe, Inc. in the USA), this is done on the basis of EU Standard Contractual Clauses pursuant to Art. 46 GDPR.",
|
"Payments are processed by our payment provider, Stripe Payments Europe, Ltd. The data necessary for payment processing is transmitted to Stripe for this purpose. Stripe's privacy policy also applies (stripe.com/privacy). Where this involves transferring data to Stripe-affiliated companies outside the EU/EEA (in particular Stripe, Inc. in the USA), this is done on the basis of EU Standard Contractual Clauses pursuant to Art. 46 GDPR.",
|
||||||
|
"If you choose PayPal or Klarna in the payment step, you are redirected to the respective provider and the order data required for payment is transmitted to them: PayPal (Europe) S.à r.l. et Cie, S.C.A., 22–24 Boulevard Royal, L-2449 Luxembourg, or Klarna Bank AB (publ), Sveavägen 46, 111 34 Stockholm, Sweden. These providers may carry out their own checks, e.g. of identity or creditworthiness; they are responsible for these themselves and their privacy notices apply. Legal basis: Art. 6(1)(b) GDPR.",
|
||||||
"Order and invoice data is retained for the legally required period due to commercial and tax record-keeping obligations (§ 257 HGB, § 147 AO of German law).",
|
"Order and invoice data is retained for the legally required period due to commercial and tax record-keeping obligations (§ 257 HGB, § 147 AO of German law).",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "5. Customer Account",
|
heading: "5. Customer Account",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"You can alternatively sign in with a Microsoft account (Microsoft Entra ID). You are redirected to Microsoft and sign in there; we never receive a password, only your email address and name in order to match your customer account. Which data Microsoft itself processes during sign-in is governed by Microsoft's privacy notice. The provider is Microsoft Ireland Operations Limited; a transfer to Microsoft Corporation in the USA may take place. Microsoft is certified under the EU-US Data Privacy Framework. Legal basis: Art. 6(1)(b) GDPR.",
|
|
||||||
"If you create a customer account, we store the data you provide (name, email, password encrypted as a hash) as well as any delivery addresses you optionally save, to give you access to your order history, invoices, and a faster checkout. The legal basis is Art. 6(1)(b) GDPR. You can have your account and associated data deleted at any time via an informal request to us.",
|
"If you create a customer account, we store the data you provide (name, email, password encrypted as a hash) as well as any delivery addresses you optionally save, to give you access to your order history, invoices, and a faster checkout. The legal basis is Art. 6(1)(b) GDPR. You can have your account and associated data deleted at any time via an informal request to us.",
|
||||||
|
"You can alternatively sign in with a Microsoft account (Microsoft Entra ID). You are redirected to Microsoft and sign in there; we never receive a password, only your email address and name in order to match your customer account. Which data Microsoft itself processes during sign-in is governed by Microsoft's privacy notice. The provider is Microsoft Ireland Operations Limited; a transfer to Microsoft Corporation in the USA may take place. Microsoft is certified under the EU-US Data Privacy Framework. Legal basis: Art. 6(1)(b) GDPR.",
|
||||||
"A technically necessary cookie is set for login; see our Cookie Policy.",
|
"A technically necessary cookie is set for login; see our Cookie Policy.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -288,6 +291,7 @@ export const en: Dictionary = {
|
|||||||
heading: "6. Newsletter (Harvest Mail)",
|
heading: "6. Newsletter (Harvest Mail)",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"If you sign up for our harvest mail, we use your email address exclusively to send information about new harvests. The legal basis is your consent (Art. 6(1)(a) GDPR). You can unsubscribe at any time via the link in every email or through your customer account.",
|
"If you sign up for our harvest mail, we use your email address exclusively to send information about new harvests. The legal basis is your consent (Art. 6(1)(a) GDPR). You can unsubscribe at any time via the link in every email or through your customer account.",
|
||||||
|
"Sign-up uses a double opt-in: you are only added once you click the link in our confirmation email. Unsubscribing withdraws your consent with effect for the future (Art. 7(3) GDPR). We then keep your address together with the time of unsubscribing so that we can demonstrate your consent and its withdrawal and send you no further harvest mail (Art. 6(1)(f) GDPR).",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -312,14 +316,36 @@ export const en: Dictionary = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "10. Cookies",
|
heading: "10. Product Reviews",
|
||||||
|
paragraphs: [
|
||||||
|
"If you write a review while signed in, we store the star rating, text, time and the link to your customer account. Once approved by us, the review appears on the product page together with your first name and the date. Legal basis: Art. 6(1)(b) GDPR; the review is kept until you ask us to delete it or your customer account is deleted.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "11. Back-in-Stock Notification",
|
||||||
|
paragraphs: [
|
||||||
|
"If you ask to be notified when a sold-out product is available again, we store your email address, the product and your language in order to send you exactly one notification once it is back in stock. Legal basis: your request (Art. 6(1)(b) GDPR). You can have the request deleted at any time by contacting us.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "12. Cookies",
|
||||||
paragraphs: ["Details on the cookies used on this website can be found in our Cookie Policy."],
|
paragraphs: ["Details on the cookies used on this website can be found in our Cookie Policy."],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "11. Your Rights",
|
heading: "13. Storage Period and Obligation to Provide Data",
|
||||||
|
paragraphs: [
|
||||||
|
"We store personal data only as long as necessary for the respective purpose or as required by statutory retention periods (for order and invoice data up to ten years under § 147 AO, § 257 HGB). The data is deleted afterwards.",
|
||||||
|
"The details required for an order are needed to conclude and perform the contract; without them no order is possible. All other details (e.g. newsletter, reviews) are voluntary.",
|
||||||
|
"No automated decision-making including profiling within the meaning of Art. 22 GDPR takes place.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "14. Your Rights",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"You have the right to access (Art. 15 GDPR), rectification (Art. 16 GDPR), erasure (Art. 17 GDPR), restriction of processing (Art. 18 GDPR), data portability (Art. 20 GDPR), and objection to processing (Art. 21 GDPR) of your personal data.",
|
"You have the right to access (Art. 15 GDPR), rectification (Art. 16 GDPR), erasure (Art. 17 GDPR), restriction of processing (Art. 18 GDPR), data portability (Art. 20 GDPR), and objection to processing (Art. 21 GDPR) of your personal data.",
|
||||||
"You also have the right to lodge a complaint with a data protection supervisory authority, e.g. the Bavarian State Office for Data Protection Supervision.",
|
"You also have the right to lodge a complaint with a data protection supervisory authority, e.g. the Bavarian State Office for Data Protection Supervision (BayLDA), Promenade 18, 91522 Ansbach, Germany, www.lda.bayern.de.",
|
||||||
|
"If you have given consent, you may withdraw it at any time with effect for the future (Art. 7(3) GDPR). This does not affect the lawfulness of processing carried out before the withdrawal.",
|
||||||
|
"Right to object: where we process data on the basis of legitimate interests (Art. 6(1)(f) GDPR), you may object at any time on grounds relating to your particular situation (Art. 21(1) GDPR). We will then no longer process the data unless we can demonstrate compelling legitimate grounds. You may object to processing for direct marketing at any time without giving reasons (Art. 21(2) GDPR). An informal message to us is sufficient.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -337,6 +363,7 @@ export const en: Dictionary = {
|
|||||||
heading: "2. Formation of Contract",
|
heading: "2. Formation of Contract",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"The presentation of products in the shop does not constitute a binding offer, but rather a non-binding invitation for you to place an order. By completing an order, you submit a binding offer to purchase, which we accept by confirming it via email or by delivering the goods.",
|
"The presentation of products in the shop does not constitute a binding offer, but rather a non-binding invitation for you to place an order. By completing an order, you submit a binding offer to purchase, which we accept by confirming it via email or by delivering the goods.",
|
||||||
|
"Until payment is completed you can correct your entries in the order form at any time; from the payment step, “Back to your details” returns you to the form. The contract is concluded in German. We store the contract text; you receive the order details with the confirmation email, and with a customer account you can view your orders there at any time. You can access, print and save these terms on this page at any time.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -346,18 +373,18 @@ export const en: Dictionary = {
|
|||||||
{
|
{
|
||||||
heading: "4. Payment Methods",
|
heading: "4. Payment Methods",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"We offer PayPal, credit card, Apple Pay, and Google Pay via our payment provider Stripe. Payment by bank transfer is available on request. See our \"Shipping & Payment\" page and our Privacy Policy for details on Stripe.",
|
"Payment is processed by our payment provider Stripe. The payment methods available (e.g. credit card, SEPA direct debit, Klarna, PayPal, Apple Pay, Google Pay) are shown during checkout and on our \"Shipping & Payment\" page. Payment by bank transfer is available on request.",
|
||||||
"The purchase price is generally due immediately upon ordering (prepayment via the chosen payment method), unless otherwise agreed.",
|
"The purchase price is generally due immediately upon ordering (prepayment via the chosen payment method), unless otherwise agreed.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "5. Delivery and Pickup",
|
heading: "5. Delivery and Pickup",
|
||||||
paragraphs: ["We deliver within Germany. Alternatively, goods can be picked up directly from us in Altenmünster by arrangement."],
|
paragraphs: ["We deliver to the delivery area stated on our \"Shipping & Payment\" page. Alternatively, goods can be picked up directly from us in Altenmünster by arrangement."],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "6. Right of Withdrawal",
|
heading: "6. Right of Withdrawal",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Consumers generally have a statutory right of withdrawal. For details, in particular exceptions for perishable goods such as honey, please see our separate Right of Withdrawal page.",
|
"Consumers generally have a statutory right of withdrawal. For details, in particular the statutory exceptions, please see our separate Right of Withdrawal page.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -391,25 +418,39 @@ export const en: Dictionary = {
|
|||||||
title: "Right of Withdrawal",
|
title: "Right of Withdrawal",
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
|
// Translation of the official German model (see de.ts); the German
|
||||||
|
// version is the legally binding one.
|
||||||
heading: "Right of Withdrawal",
|
heading: "Right of Withdrawal",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"You have the right to withdraw from this contract within fourteen days without giving any reason. The withdrawal period is fourteen days from the day on which you, or a third party named by you, took possession of the goods.",
|
"You have the right to withdraw from this contract within fourteen days without giving any reason.",
|
||||||
"To exercise your right of withdrawal, you must inform us of your decision to withdraw from this contract by means of a clear statement (e.g. by email).",
|
"The withdrawal period is fourteen days from the day on which you, or a third party other than the carrier named by you, took possession of the goods.",
|
||||||
|
"To exercise your right of withdrawal, you must inform us ({{anbieter}}) of your decision to withdraw from this contract by means of a clear statement (e.g. a letter sent by post or an email). You may use the model withdrawal form below, but it is not obligatory.",
|
||||||
|
"To meet the withdrawal deadline, it is sufficient for you to send your communication concerning your exercise of the right of withdrawal before the withdrawal period has expired.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Consequences of Withdrawal",
|
heading: "Consequences of Withdrawal",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"If you withdraw from this contract, we will refund all payments we received from you without undue delay, and no later than fourteen days from the day on which we received notice of your withdrawal.",
|
"If you withdraw from this contract, we shall reimburse to you all payments received from you, including the costs of delivery (with the exception of the supplementary costs resulting from your choice of a type of delivery other than the least expensive type of standard delivery offered by us), without undue delay and in any event not later than fourteen days from the day on which we are informed about your decision to withdraw from this contract. We will carry out such reimbursement using the same means of payment as you used for the initial transaction, unless you have expressly agreed otherwise; in any event, you will not incur any fees as a result of such reimbursement.",
|
||||||
// Siehe Begruendung in de.ts — Wortlaut der amtlichen
|
"We may withhold reimbursement until we have received the goods back or you have supplied evidence of having sent back the goods, whichever is the earliest.",
|
||||||
// Muster-Widerrufsbelehrung, hier nur uebersetzt.
|
"You shall send back the goods or hand them over to us without undue delay and in any event not later than fourteen days from the day on which you communicate your withdrawal from this contract to us. The deadline is met if you send back the goods before the period of fourteen days has expired.",
|
||||||
"You bear the direct cost of returning the goods.",
|
"You bear the direct cost of returning the goods.",
|
||||||
|
"You are only liable for any diminished value of the goods resulting from handling other than what is necessary to establish the nature, characteristics and functioning of the goods.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Exclusion of the Right of Withdrawal",
|
heading: "Exclusion of the Right of Withdrawal",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"The right of withdrawal does not apply to the delivery of goods that can spoil quickly or whose expiry date would soon be exceeded, nor to sealed goods that are not suitable for return for health or hygiene reasons if their seal has been removed after delivery.",
|
"The right of withdrawal does not apply to contracts for the supply of sealed goods which are not suitable for return due to health protection or hygiene reasons if they were unsealed after delivery (§ 312g(2) No. 3 BGB). This applies in particular to opened honey jars whose tamper-evident seal has been broken.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: "Model withdrawal form",
|
||||||
|
paragraphs: [
|
||||||
|
"(Complete and return this form only if you wish to withdraw from the contract.)",
|
||||||
|
"To: {{anbieter}}",
|
||||||
|
"- I/We (*) hereby give notice that I/We (*) withdraw from my/our (*) contract of sale of the following goods (*)/for the provision of the following service (*)\n- Ordered on (*)/received on (*)\n- Name of consumer(s)\n- Address of consumer(s)\n- Signature of consumer(s) (only if this form is notified on paper)\n- Date",
|
||||||
|
"(*) Delete as appropriate.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -486,17 +527,17 @@ export const en: Dictionary = {
|
|||||||
heading: "Local storage (localStorage)",
|
heading: "Local storage (localStorage)",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"The following entries are kept in your browser's local storage. Unlike cookies, they are not automatically sent to us with every page request and do not expire on their own: they remain until you delete them (via your browser's “clear site data” setting) or the website removes them. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(b) or (f) GDPR.",
|
"The following entries are kept in your browser's local storage. Unlike cookies, they are not automatically sent to us with every page request and do not expire on their own: they remain until you delete them (via your browser's “clear site data” setting) or the website removes them. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(b) or (f) GDPR.",
|
||||||
"wabenhain-cart – Your cart (items, quantities, prices). It is only transmitted to us when you submit an order, and it is emptied after a successful order.",
|
"wabenhain-cart – Your cart (items, quantities, prices). It is only transmitted to us when you submit an order, and it is emptied after a successful order. Storage period: until your next order or until you empty it yourself.",
|
||||||
"wabenhain-recently-viewed – The identifiers of up to 6 recently viewed products, so they can be shown to you again on product pages. Only these product identifiers are requested for display; we store no browsing history on our servers.",
|
"wabenhain-recently-viewed – The identifiers of up to 6 recently viewed products, so they can be shown to you again on product pages. Only these product identifiers are requested for display; we store no browsing history on our servers. Storage period: until you clear the site data in your browser.",
|
||||||
"wabenhain-cookie-consent – Your choice in the cookie banner (“Essential only” or “Accept all”), so the banner does not reappear on every visit and we can demonstrate your decision. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(c) GDPR in conjunction with Art. 7(1) GDPR.",
|
"wabenhain-cookie-consent – Your choice in the cookie banner (“Essential only” or “Accept all”), so the banner does not reappear on every visit and we can demonstrate your decision. Storage period: until you change your choice or clear the site data. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(c) GDPR in conjunction with Art. 7(1) GDPR.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "Third-party cookies",
|
heading: "Third-party cookies",
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
"Stripe (payment processing): Only in the payment step of checkout does your browser load Stripe's payment form. Stripe sets its own cookies for fraud prevention and secure payment processing, in particular __stripe_mid (storage period 1 year) and __stripe_sid (storage period 30 minutes), as well as further cookies within the payment form on Stripe domains. Stripe determines the names and lifetimes of these cookies. Provider: Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Ireland. Data may be transferred to Stripe, Inc. in the USA on the basis of EU standard contractual clauses (Art. 46 GDPR). Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(b) GDPR. More information: stripe.com/privacy and stripe.com/legal/cookies-policy.",
|
"Stripe (payment processing): Only in the payment step of checkout does your browser load Stripe's payment form. Stripe sets its own cookies for fraud prevention and secure payment processing, in particular __stripe_mid (storage period 1 year) and __stripe_sid (storage period 30 minutes), as well as further cookies within the payment form on Stripe domains. Stripe determines the names and lifetimes of these cookies. Provider: Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Ireland. Data may be transferred to Stripe, Inc. in the USA on the basis of EU standard contractual clauses (Art. 46 GDPR). Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(b) GDPR. More information: stripe.com/privacy and stripe.com/legal/cookies-policy.",
|
||||||
"Microsoft (sign-in with a Microsoft account): If you choose “Sign in with Microsoft”, you are redirected to a Microsoft sign-in page. Microsoft (Microsoft Ireland Operations Limited) is responsible for the cookies set there; Microsoft's privacy statement applies. Microsoft sets no cookies on our website.",
|
"Microsoft (sign-in with a Microsoft account): If you choose “Sign in with Microsoft”, you are redirected to a Microsoft sign-in page. Microsoft is responsible for the cookies set there; Microsoft's privacy statement applies. Microsoft sets no cookies on our website. Provider: Microsoft Ireland Operations Limited, One Microsoft Place, South County Business Park, Leopardstown, Dublin 18, Ireland.",
|
||||||
"Cloudflare Turnstile (form protection): On pages with forms (sign-in, registration, password reset, two-factor verification, checkout, newsletter, business enquiry, back-in-stock notification), your browser loads a script from challenges.cloudflare.com. It distinguishes input made by humans from input made by automated programs. Your IP address and technical characteristics of your browser are transmitted to Cloudflare, Inc. (USA) in the process. According to the provider, Turnstile sets no advertising cookies and builds no cross-site user profile. Cloudflare is certified under the EU-US Data Privacy Framework. Details are in our privacy policy.",
|
"Cloudflare Turnstile (form protection): On pages with forms (sign-in, registration, password reset, two-factor verification, checkout, newsletter, business enquiry, back-in-stock notification), your browser loads a script from challenges.cloudflare.com. It distinguishes input made by humans from input made by automated programs. Your IP address and technical characteristics of your browser are transmitted to Cloudflare, Inc. (USA) in the process. According to the provider, Turnstile sets no advertising cookies and builds no cross-site user profile. Cloudflare is certified under the EU-US Data Privacy Framework. Provider: Cloudflare, Inc., 101 Townsend Street, San Francisco, CA 94107, USA. Legal basis: § 25(2) No. 2 TDDDG and Art. 6(1)(f) GDPR. More information: see the “Cloudflare Turnstile” section of our privacy policy.",
|
||||||
"Google reviews: We retrieve the reviews on our homepage server-side via the Google Places API. Your browser does not connect to Google, and Google sets no cookie.",
|
"Google reviews: We retrieve the reviews on our homepage server-side via the Google Places API. Your browser does not connect to Google, and Google sets no cookie.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ export interface Dictionary {
|
|||||||
};
|
};
|
||||||
legalPage: {
|
legalPage: {
|
||||||
reviewNotice: string;
|
reviewNotice: string;
|
||||||
|
tocLabel: string;
|
||||||
|
revisionLabel: string;
|
||||||
};
|
};
|
||||||
notFoundPage: {
|
notFoundPage: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { extractRevision, parseLegalParagraph, sectionAnchors, splitSentences, tokenizeInline } from "./legal-blocks";
|
||||||
|
|
||||||
|
describe("parseLegalParagraph", () => {
|
||||||
|
it("keeps ordinary prose as a text block", () => {
|
||||||
|
expect(parseLegalParagraph("Cookies sind kleine Textdateien. Sie speichern Einstellungen.")).toEqual({
|
||||||
|
kind: "text",
|
||||||
|
text: "Cookies sind kleine Textdateien. Sie speichern Einstellungen.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turns a paragraph whose lines all start with a dash into a list", () => {
|
||||||
|
expect(parseLegalParagraph("- Auskunft\n- Berichtigung\n• Löschung")).toEqual({
|
||||||
|
kind: "list",
|
||||||
|
items: ["Auskunft", "Berichtigung", "Löschung"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat a single dashed line mixed with prose as a list", () => {
|
||||||
|
expect(parseLegalParagraph("Einleitung\n- Punkt").kind).toBe("text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognises a storage entry and pulls labelled facts out of it", () => {
|
||||||
|
const block = parseLegalParagraph(
|
||||||
|
"wabenhain_session – Hält dich angemeldet. Inhalt: signierte Kennung, keine Passwörter. Speicherdauer: 30 Tage; wird beim Abmelden gelöscht."
|
||||||
|
);
|
||||||
|
expect(block).toEqual({
|
||||||
|
kind: "entry",
|
||||||
|
name: "wabenhain_session",
|
||||||
|
note: null,
|
||||||
|
text: "Hält dich angemeldet.",
|
||||||
|
facts: [
|
||||||
|
{ label: "Inhalt", value: "signierte Kennung, keine Passwörter." },
|
||||||
|
{ label: "Speicherdauer", value: "30 Tage; wird beim Abmelden gelöscht." },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps abbreviations such as Art. and Abs. inside one fact", () => {
|
||||||
|
const block = parseLegalParagraph(
|
||||||
|
"theme – Speichert die Darstellung. Rechtsgrundlage: § 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. f DSGVO. Wird sonst gelöscht."
|
||||||
|
);
|
||||||
|
expect(block.kind === "entry" && block.facts).toEqual([
|
||||||
|
{ label: "Rechtsgrundlage", value: "§ 25 Abs. 2 Nr. 2 TDDDG und Art. 6 Abs. 1 lit. f DSGVO." },
|
||||||
|
]);
|
||||||
|
expect(block.kind === "entry" && block.text).toBe("Speichert die Darstellung. Wird sonst gelöscht.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognises a provider entry written as 'Name (Zweck): Text'", () => {
|
||||||
|
const block = parseLegalParagraph("Stripe (Zahlungsabwicklung): Lädt das Formular. Anbieter: Stripe Payments Europe, Ltd., Dublin.");
|
||||||
|
expect(block).toMatchObject({ kind: "entry", name: "Stripe", note: "Zahlungsabwicklung", text: "Lädt das Formular." });
|
||||||
|
expect(block.kind === "entry" && block.facts).toEqual([{ label: "Anbieter", value: "Stripe Payments Europe, Ltd., Dublin." }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("recognises English fact labels", () => {
|
||||||
|
const block = parseLegalParagraph("locale – Stores your language. Storage period: 1 year.");
|
||||||
|
expect(block.kind === "entry" && block.facts).toEqual([{ label: "Storage period", value: "1 year." }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mistake a sentence with a spaced dash for an entry", () => {
|
||||||
|
expect(parseLegalParagraph("Wir versenden schnell – meist in zwei Tagen.").kind).toBe("text");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("splitSentences", () => {
|
||||||
|
it("does not split after z. B. or Art.", () => {
|
||||||
|
expect(splitSentences("Etwa z. B. Chrome. Nach Art. 6 DSGVO. Ende.")).toEqual(["Etwa z. B. Chrome.", "Nach Art. 6 DSGVO.", "Ende."]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractRevision", () => {
|
||||||
|
it("moves a trailing 'Stand:' line out of the sections", () => {
|
||||||
|
const { sections, revision } = extractRevision([
|
||||||
|
{ heading: "A", paragraphs: ["Text"] },
|
||||||
|
{ heading: "B", paragraphs: ["Mehr", "Stand: September 2026"] },
|
||||||
|
]);
|
||||||
|
expect(revision).toBe("September 2026");
|
||||||
|
expect(sections).toEqual([
|
||||||
|
{ heading: "A", paragraphs: ["Text"] },
|
||||||
|
{ heading: "B", paragraphs: ["Mehr"] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops a section that only held the revision line", () => {
|
||||||
|
const { sections, revision } = extractRevision([
|
||||||
|
{ heading: "A", paragraphs: ["Text"] },
|
||||||
|
{ heading: "Stand", paragraphs: ["Last updated: May 2026"] },
|
||||||
|
]);
|
||||||
|
expect(revision).toBe("May 2026");
|
||||||
|
expect(sections).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when no revision is given", () => {
|
||||||
|
expect(extractRevision([{ heading: "A", paragraphs: ["Text"] }]).revision).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sectionAnchors", () => {
|
||||||
|
it("builds readable, unique anchors from headings", () => {
|
||||||
|
expect(sectionAnchors(["1. Verantwortlicher", "Deine Rechte", "Deine Rechte", "Über uns & mehr"])).toEqual([
|
||||||
|
"verantwortlicher",
|
||||||
|
"deine-rechte",
|
||||||
|
"deine-rechte-2",
|
||||||
|
"ueber-uns-mehr",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to a numbered anchor for headings without letters", () => {
|
||||||
|
expect(sectionAnchors(["§§", ""])).toEqual(["abschnitt-1", "abschnitt-2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tokenizeInline", () => {
|
||||||
|
it("links web addresses with a path and e-mail addresses", () => {
|
||||||
|
expect(tokenizeInline("Siehe stripe.com/de/privacy oder schreib an [email protected].")).toEqual([
|
||||||
|
{ kind: "text", value: "Siehe " },
|
||||||
|
{ kind: "link", value: "stripe.com/de/privacy", href: "https://stripe.com/de/privacy" },
|
||||||
|
{ kind: "text", value: " oder schreib an " },
|
||||||
|
{ kind: "link", value: "[email protected]", href: "mailto:[email protected]" },
|
||||||
|
{ kind: "text", value: "." },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps explicit https links and never links other schemes", () => {
|
||||||
|
expect(tokenizeInline("https://example.org/a javascript:alert(1)")).toEqual([
|
||||||
|
{ kind: "link", value: "https://example.org/a", href: "https://example.org/a" },
|
||||||
|
{ kind: "text", value: " javascript:alert(1)" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a bare domain without path as text", () => {
|
||||||
|
expect(tokenizeInline("wabenhain.com")).toEqual([{ kind: "text", value: "wabenhain.com" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Liest aus dem Klartext der Rechtsseiten (Admin-Bereich → Inhalte) eine
|
||||||
|
* leichte Struktur heraus, ohne dass im Admin eine Auszeichnungssprache
|
||||||
|
* noetig wird. Erkannt wird nur, was eindeutig ist — alles andere bleibt
|
||||||
|
* ein normaler Absatz, damit eigener Text nie verfaelscht dargestellt wird:
|
||||||
|
*
|
||||||
|
* - Absatz, dessen Zeilen ALLE mit "- " oder "• " beginnen → Aufzaehlung
|
||||||
|
* - "cookie_name – Beschreibung. Speicherdauer: 1 Jahr." → Eintrag mit Fakten
|
||||||
|
* - "Anbieter (Zweck): Beschreibung. Anbieter: …" → Eintrag mit Fakten
|
||||||
|
* - "Stand: September 2026" als eigener Absatz → Datum im Seitenkopf
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface LegalSectionContent {
|
||||||
|
heading: string;
|
||||||
|
paragraphs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegalFact {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LegalBlock =
|
||||||
|
| { kind: "text"; text: string }
|
||||||
|
| { kind: "list"; items: string[] }
|
||||||
|
| { kind: "entry"; name: string; note: string | null; text: string; facts: LegalFact[] };
|
||||||
|
|
||||||
|
export type InlineToken = { kind: "text"; value: string } | { kind: "link"; value: string; href: string };
|
||||||
|
|
||||||
|
const LIST_LINE = /^\s*[-•]\s+(.+)$/;
|
||||||
|
// Technischer Name ohne Leerzeichen, dann " – " (Halbgeviertstrich mit
|
||||||
|
// Leerzeichen): so schreibt die Cookie-Richtlinie ihre Eintraege.
|
||||||
|
const CODE_ENTRY = /^([A-Za-z0-9_.-]{3,40}) – ([\s\S]+)$/;
|
||||||
|
// "Stripe (Zahlungsabwicklung): …" — Anbieter mit Zweck in Klammern.
|
||||||
|
const LABEL_ENTRY = /^([A-ZÄÖÜ][\wÄÖÜäöüß .&-]{1,30}?) \(([^()]{2,60})\): ([\s\S]+)$/;
|
||||||
|
const FACT_LABELS = [
|
||||||
|
"Inhalt",
|
||||||
|
"Speicherdauer",
|
||||||
|
"Rechtsgrundlage",
|
||||||
|
"Anbieter",
|
||||||
|
"Weitere Informationen",
|
||||||
|
"Content",
|
||||||
|
"Storage period",
|
||||||
|
"Legal basis",
|
||||||
|
"Provider",
|
||||||
|
"More information",
|
||||||
|
];
|
||||||
|
const FACT = new RegExp(`^(${FACT_LABELS.join("|")}):\\s+([\\s\\S]+)$`);
|
||||||
|
const REVISION = /^(Stand|Zuletzt aktualisiert|Last updated)\s*:\s*(.+)$/i;
|
||||||
|
// Kuerzel, nach denen ein Punkt KEIN Satzende ist (Art. 6, Abs. 1, z. B.).
|
||||||
|
const ABBREVIATIONS = new Set(["art", "abs", "nr", "lit", "bzw", "ggf", "vgl", "ca", "inkl", "evtl", "usw", "etc", "ltd", "inc", "co", "no", "st", "str"]);
|
||||||
|
|
||||||
|
export function splitSentences(text: string): string[] {
|
||||||
|
const sentences: string[] = [];
|
||||||
|
let start = 0;
|
||||||
|
for (const match of text.matchAll(/[.!?]\s+/g)) {
|
||||||
|
const end = match.index + 1;
|
||||||
|
const next = text[match.index + match[0].length] ?? "";
|
||||||
|
const word = /([\p{L}\d]+)$/u.exec(text.slice(start, match.index))?.[1] ?? "";
|
||||||
|
const isAbbreviation = word.length === 1 || /^\d+$/.test(word) || ABBREVIATIONS.has(word.toLowerCase());
|
||||||
|
if (isAbbreviation || !/[\p{Lu}„"§]/u.test(next)) continue;
|
||||||
|
sentences.push(text.slice(start, end).trim());
|
||||||
|
start = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
const rest = text.slice(start).trim();
|
||||||
|
if (rest) sentences.push(rest);
|
||||||
|
return sentences;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toEntry(name: string, note: string | null, body: string): LegalBlock {
|
||||||
|
const facts: LegalFact[] = [];
|
||||||
|
const prose: string[] = [];
|
||||||
|
for (const sentence of splitSentences(body)) {
|
||||||
|
const fact = FACT.exec(sentence);
|
||||||
|
if (fact) facts.push({ label: fact[1], value: fact[2] });
|
||||||
|
else prose.push(sentence);
|
||||||
|
}
|
||||||
|
return { kind: "entry", name, note, text: prose.join(" "), facts };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseLegalParagraph(paragraph: string): LegalBlock {
|
||||||
|
const text = paragraph.trim();
|
||||||
|
const lines = text.split("\n");
|
||||||
|
if (lines.length > 1 && lines.every((line) => LIST_LINE.test(line))) {
|
||||||
|
return { kind: "list", items: lines.map((line) => LIST_LINE.exec(line)![1].trim()) };
|
||||||
|
}
|
||||||
|
const code = CODE_ENTRY.exec(text);
|
||||||
|
if (code) return toEntry(code[1], null, code[2]);
|
||||||
|
const labelled = LABEL_ENTRY.exec(text);
|
||||||
|
if (labelled) return toEntry(labelled[1], labelled[2], labelled[3]);
|
||||||
|
return { kind: "text", text };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Holt "Stand: …" aus dem Text, damit es im Seitenkopf statt als loser Satz am Ende steht. */
|
||||||
|
export function extractRevision(sections: LegalSectionContent[]): { sections: LegalSectionContent[]; revision: string | null } {
|
||||||
|
let revision: string | null = null;
|
||||||
|
const cleaned = sections
|
||||||
|
.map((section) => ({
|
||||||
|
...section,
|
||||||
|
paragraphs: section.paragraphs.filter((p) => {
|
||||||
|
const match = p.includes("\n") ? null : REVISION.exec(p.trim());
|
||||||
|
if (match && revision === null) revision = match[2].trim();
|
||||||
|
return !match;
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter((section) => section.paragraphs.length > 0);
|
||||||
|
return { sections: cleaned, revision };
|
||||||
|
}
|
||||||
|
|
||||||
|
const UMLAUTS: Record<string, string> = { ä: "ae", ö: "oe", ü: "ue", ß: "ss" };
|
||||||
|
|
||||||
|
export function sectionAnchors(headings: string[]): string[] {
|
||||||
|
const seen = new Map<string, number>();
|
||||||
|
return headings.map((heading, i) => {
|
||||||
|
const base =
|
||||||
|
heading
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^\s*\d+[.)]?\s*/, "")
|
||||||
|
.replace(/[äöüß]/g, (c) => UMLAUTS[c])
|
||||||
|
.normalize("NFKD")
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "") || `abschnitt-${i + 1}`;
|
||||||
|
const count = (seen.get(base) ?? 0) + 1;
|
||||||
|
seen.set(base, count);
|
||||||
|
return count === 1 ? base : `${base}-${count}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur https/http-Adressen, Domains MIT Pfad (stripe.com/de/privacy) und
|
||||||
|
// E-Mail-Adressen werden verlinkt. Nackte Domains bleiben Text, andere
|
||||||
|
// Schemata (javascript:, data:) werden nie zu Links.
|
||||||
|
const INLINE_LINK =
|
||||||
|
/(https?:\/\/[^\s<>"]*[^\s<>".,;:)])|((?<![\w@/.])(?:[a-z0-9-]+\.)+[a-z]{2,}\/[^\s<>"]*[^\s<>".,;:)])|([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})/g;
|
||||||
|
|
||||||
|
export function tokenizeInline(text: string): InlineToken[] {
|
||||||
|
const tokens: InlineToken[] = [];
|
||||||
|
let last = 0;
|
||||||
|
for (const match of text.matchAll(INLINE_LINK)) {
|
||||||
|
if (match.index > last) tokens.push({ kind: "text", value: text.slice(last, match.index) });
|
||||||
|
const [value, url, domainPath, email] = match;
|
||||||
|
const href = url ?? (domainPath ? `https://${domainPath}` : `mailto:${email}`);
|
||||||
|
tokens.push({ kind: "link", value, href });
|
||||||
|
last = match.index + value.length;
|
||||||
|
}
|
||||||
|
if (last < text.length) tokens.push({ kind: "text", value: text.slice(last) });
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user