- Vielen Dank!
+ {block.successMessage || "Vielen Dank!"}
Deine Nachricht ist angekommen. Ich melde mich innerhalb von 24 Stunden bei dir.
diff --git a/src/app/faq/FAQAccordion.tsx b/src/components/blocks/FAQAccordion.tsx
similarity index 90%
rename from src/app/faq/FAQAccordion.tsx
rename to src/components/blocks/FAQAccordion.tsx
index 9351bfe..3eadcde 100644
--- a/src/app/faq/FAQAccordion.tsx
+++ b/src/components/blocks/FAQAccordion.tsx
@@ -10,6 +10,7 @@ interface FAQ {
interface FAQAccordionProps {
faqs: FAQ[]
+ expandFirst?: boolean
}
function extractText(richText: unknown): string {
@@ -31,8 +32,8 @@ function extractText(richText: unknown): string {
return walk(root.children)
}
-export function FAQAccordion({ faqs }: FAQAccordionProps) {
- const [openIndex, setOpenIndex] = useState(null)
+export function FAQAccordion({ faqs, expandFirst = false }: FAQAccordionProps) {
+ const [openIndex, setOpenIndex] = useState(expandFirst ? 0 : null)
return (
diff --git a/src/components/blocks/FAQBlock.tsx b/src/components/blocks/FAQBlock.tsx
new file mode 100644
index 0000000..b4dd66d
--- /dev/null
+++ b/src/components/blocks/FAQBlock.tsx
@@ -0,0 +1,52 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { fetchFAQs } from "@/lib/api"
+import { ScrollReveal } from "@/components/ScrollReveal"
+import { FAQAccordion } from "./FAQAccordion"
+
+interface FAQBlockProps {
+ block: {
+ title?: string
+ sourceMode?: string
+ displayMode?: string
+ category?: string
+ limit?: number
+ layout?: string
+ expandFirst?: boolean
+ backgroundColor?: string
+ }
+}
+
+export async function FAQBlock({ block }: FAQBlockProps) {
+ const faqs = await fetchFAQs(block.category || undefined)
+ const items = block.limit ? faqs.slice(0, block.limit) : faqs
+
+ if (!items.length) return null
+
+ const bgMap: Record
= {
+ light: "bg-creme",
+ dark: "bg-dark-wine",
+ white: "bg-white",
+ }
+ const bg = bgMap[block.backgroundColor || "light"] || bgMap.light
+ const isDark = block.backgroundColor === "dark"
+
+ return (
+
+
+ {block.title && (
+
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/blocks/HeroBlock.tsx b/src/components/blocks/HeroBlock.tsx
new file mode 100644
index 0000000..634d857
--- /dev/null
+++ b/src/components/blocks/HeroBlock.tsx
@@ -0,0 +1,97 @@
+"use client"
+
+import { useEffect, useState } from "react"
+
+interface HeroBlockProps {
+ block: {
+ headline?: string
+ subline?: string
+ alignment?: string
+ overlay?: boolean
+ cta?: { text?: string; link?: string; style?: string }
+ backgroundImage?: { url?: string; alt?: string } | number
+ }
+ isFirst?: boolean
+}
+
+export function HeroBlock({ block, isFirst = false }: HeroBlockProps) {
+ const [visible, setVisible] = useState(false)
+
+ useEffect(() => {
+ const timer = setTimeout(() => setVisible(true), 100)
+ return () => clearTimeout(timer)
+ }, [])
+
+ const bgImage = typeof block.backgroundImage === "object" && block.backgroundImage?.url
+ ? block.backgroundImage.url
+ : null
+ const cmsUrl = process.env.NEXT_PUBLIC_CMS_URL || "https://pl.porwoll.tech"
+ const bgUrl = bgImage ? (bgImage.startsWith("http") ? bgImage : cmsUrl + bgImage) : null
+
+ const alignment = block.alignment || "center"
+ const alignClass = alignment === "left" ? "text-left items-start" : alignment === "right" ? "text-right items-end" : "text-center items-center"
+
+ // Split headline for Sensual / Moment style rendering
+ const headlineParts = block.headline?.split(/\s+/) || []
+ const isHomeHero = isFirst && headlineParts.length <= 3
+
+ return (
+
+ {/* Gradient overlays */}
+ {block.overlay !== false && (
+ <>
+
+
+ {bgUrl && }
+ >
+ )}
+
+
+ {/* Decorative line */}
+
+
+ {isHomeHero && headlineParts.length >= 2 ? (
+
+
+ {headlineParts[0]}
+
+
+ {headlineParts.slice(1).join(" ")}
+
+
+ ) : (
+
+ {block.headline}
+
+ )}
+
+ {block.subline && (
+
+ {block.subline}
+
+ )}
+
+ {block.cta?.text && (
+
+ )}
+
+
+ {/* Scroll indicator (only on first/full-screen hero) */}
+ {isFirst && (
+
+ )}
+
+ )
+}
diff --git a/src/components/blocks/ImageTextBlock.tsx b/src/components/blocks/ImageTextBlock.tsx
new file mode 100644
index 0000000..1f24542
--- /dev/null
+++ b/src/components/blocks/ImageTextBlock.tsx
@@ -0,0 +1,69 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { RichText } from "./RichText"
+import { ScrollReveal } from "@/components/ScrollReveal"
+
+interface ImageTextBlockProps {
+ block: {
+ image?: { url?: string; alt?: string } | number
+ imagePosition?: string
+ headline?: string
+ content?: unknown
+ cta?: { text?: string; link?: string }
+ }
+}
+
+export function ImageTextBlock({ block }: ImageTextBlockProps) {
+ const cmsUrl = process.env.NEXT_PUBLIC_CMS_URL || "https://pl.porwoll.tech"
+ const imgObj = typeof block.image === "object" ? block.image : null
+ const imageUrl = imgObj?.url ? (imgObj.url.startsWith("http") ? imgObj.url : cmsUrl + imgObj.url) : null
+ const isRight = block.imagePosition === "right"
+
+ return (
+
+
+
+ {/* Image */}
+
+ {imageUrl ? (
+
+

+
+ ) : (
+
+ )}
+
+
+ {/* Text */}
+
+
+ {block.headline && (
+ <>
+
+ {block.imagePosition === "left" ? "Philosophie" : "Ueber mich"}
+
+
+ {block.headline}
+
+
+ >
+ )}
+
+ {block.cta?.text && (
+
+ {block.cta.text}
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/blocks/PostsListBlock.tsx b/src/components/blocks/PostsListBlock.tsx
new file mode 100644
index 0000000..180e7d0
--- /dev/null
+++ b/src/components/blocks/PostsListBlock.tsx
@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { fetchPosts } from "@/lib/api"
+import { ScrollReveal } from "@/components/ScrollReveal"
+
+interface PostsListBlockProps {
+ block: {
+ title?: string
+ layout?: string
+ columns?: string
+ limit?: number
+ showExcerpt?: boolean
+ showDate?: boolean
+ showAuthor?: boolean
+ }
+}
+
+export async function PostsListBlock({ block }: PostsListBlockProps) {
+ const limit = block.limit || 3
+ const posts = await fetchPosts(limit)
+
+ if (!posts.length) return null
+
+ const cols = block.columns === "2" ? "min-[601px]:grid-cols-2" : "min-[601px]:grid-cols-2 min-[901px]:grid-cols-3"
+ const cmsUrl = process.env.NEXT_PUBLIC_CMS_URL || "https://pl.porwoll.tech"
+
+ return (
+
+
+ {block.title && (
+
+
+
Journal
+
+ {block.title}
+
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/blocks/PricingBlock.tsx b/src/components/blocks/PricingBlock.tsx
new file mode 100644
index 0000000..a3fb181
--- /dev/null
+++ b/src/components/blocks/PricingBlock.tsx
@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { ScrollReveal } from "@/components/ScrollReveal"
+
+interface PricingBlockProps {
+ block: {
+ title?: string
+ description?: string
+ pricingType?: string
+ currency?: string
+ showCurrencyBefore?: boolean
+ plans?: any[]
+ backgroundColor?: string
+ cardStyle?: string
+ }
+}
+
+export function PricingBlock({ block }: PricingBlockProps) {
+ const plans = block.plans || []
+ if (!plans.length) return null
+
+ const currency = block.currency || "\u20AC"
+ const before = block.showCurrencyBefore !== false
+
+ return (
+
+
+ {block.title && (
+
+
+
Investition
+
+ {block.title}
+
+ {block.description && (
+
+ {block.description}
+
+ )}
+
+
+
+ )}
+
+ {plans.map((plan: any, i: number) => (
+
+
+ {plan.isPopular && plan.popularLabel && (
+
+
+ {plan.popularLabel}
+
+
+ )}
+
{plan.name}
+
+ {before ? currency : ""}{plan.price}{before ? "" : currency}
+
+
+ {plan.features?.map((f: any, j: number) => (
+ -
+ ✓
+ {typeof f === "string" ? f : f.feature || f.text}
+
+ ))}
+
+ {plan.ctaText && (
+
+ {plan.ctaText}
+
+ )}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/blocks/RichText.tsx b/src/components/blocks/RichText.tsx
new file mode 100644
index 0000000..4297e58
--- /dev/null
+++ b/src/components/blocks/RichText.tsx
@@ -0,0 +1,111 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { type ReactNode } from "react"
+
+interface RichTextProps {
+ content: unknown
+ className?: string
+}
+
+function renderText(node: any): ReactNode {
+ if (!node.text && node.text !== "") return null
+ let el: ReactNode = node.text
+
+ const format = node.format || 0
+ if (format & 1) el = {el}
+ if (format & 2) el = {el}
+ if (format & 4) el = {el}
+ if (format & 8) el = {el}
+ if (format & 16) el = {el}
+
+ return el
+}
+
+function renderNode(node: any, index: number): ReactNode {
+ if (!node) return null
+
+ if (node.type === "text") {
+ return {renderText(node)}
+ }
+
+ if (node.type === "linebreak") {
+ return
+ }
+
+ if (node.type === "link") {
+ const url = node.fields?.url || node.url || "#"
+ const newTab = node.fields?.newTab || node.newTab
+ return (
+
+ {node.children?.map((child: any, i: number) => renderNode(child, i))}
+
+ )
+ }
+
+ const children = node.children?.map((child: any, i: number) => renderNode(child, i))
+
+ if (node.type === "heading") {
+ const tag = node.tag || "h2"
+ const sizes: Record = {
+ h1: "text-[clamp(2.2rem,4vw,3.5rem)]",
+ h2: "text-[clamp(1.8rem,3.5vw,2.8rem)]",
+ h3: "text-[clamp(1.4rem,2.5vw,2rem)]",
+ h4: "text-[clamp(1.2rem,2vw,1.6rem)]",
+ h5: "text-[clamp(1rem,1.5vw,1.3rem)]",
+ h6: "text-[1rem]",
+ }
+ const className = `font-playfair text-bordeaux mb-4 ${sizes[tag] || sizes.h2}`
+
+ switch (tag) {
+ case "h1": return {children}
+ case "h2": return {children}
+ case "h3": return {children}
+ case "h4": return {children}
+ case "h5": return {children}
+ case "h6": return {children}
+ default: return {children}
+ }
+ }
+
+ if (node.type === "paragraph") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (node.type === "list") {
+ const Tag = node.listType === "number" ? "ol" : "ul"
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (node.type === "listitem") {
+ return {children}
+ }
+
+ // Fallback: render children if any
+ if (children) return <>{children}>
+ return null
+}
+
+export function RichText({ content, className }: RichTextProps) {
+ if (!content || typeof content !== "object") return null
+
+ const root = (content as any).root
+ if (!root?.children?.length) return null
+
+ return (
+
+ {root.children.map((node: any, i: number) => renderNode(node, i))}
+
+ )
+}
diff --git a/src/components/blocks/TestimonialsBlock.tsx b/src/components/blocks/TestimonialsBlock.tsx
new file mode 100644
index 0000000..925238d
--- /dev/null
+++ b/src/components/blocks/TestimonialsBlock.tsx
@@ -0,0 +1,63 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { fetchTestimonials } from "@/lib/api"
+import { ScrollReveal } from "@/components/ScrollReveal"
+
+interface TestimonialsBlockProps {
+ block: {
+ title?: string
+ layout?: string
+ columns?: string
+ displayMode?: string
+ limit?: number
+ backgroundColor?: string
+ }
+}
+
+export async function TestimonialsBlock({ block }: TestimonialsBlockProps) {
+ const testimonials = await fetchTestimonials()
+ const items = block.limit ? testimonials.slice(0, block.limit) : testimonials
+
+ if (!items.length) return null
+
+ const cols = block.columns === "2" ? "min-[901px]:grid-cols-2" : "min-[901px]:grid-cols-3"
+
+ return (
+
+
+ {block.title && (
+
+
+
Kundenstimmen
+
+ {block.title}
+
+
+
+
+ )}
+
+ {items.map((t: any, i: number) => (
+
+
+
“
+
+ {t.quote}
+
+
+
+ {t.author}
+
+ {t.role && (
+
+ {t.role}
+
+ )}
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/blocks/TextBlock.tsx b/src/components/blocks/TextBlock.tsx
new file mode 100644
index 0000000..d7c720b
--- /dev/null
+++ b/src/components/blocks/TextBlock.tsx
@@ -0,0 +1,27 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { RichText } from "./RichText"
+
+interface TextBlockProps {
+ block: {
+ content?: unknown
+ width?: string
+ }
+}
+
+const widthMap: Record = {
+ narrow: "max-w-[620px]",
+ medium: "max-w-[720px]",
+ full: "max-w-[1280px]",
+}
+
+export function TextBlock({ block }: TextBlockProps) {
+ const width = widthMap[block.width || "medium"] || widthMap.medium
+
+ return (
+
+ )
+}
diff --git a/src/components/sections/AboutPreview.tsx b/src/components/sections/AboutPreview.tsx
deleted file mode 100644
index 3a25752..0000000
--- a/src/components/sections/AboutPreview.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { ScrollReveal } from "@/components/ScrollReveal"
-
-export function AboutPreview() {
- return (
-
-
-
-
- {/* Image placeholder */}
-
-
- {/* Text */}
-
-
Über mich
-
- Jede Frau verdient es,{" "}
- sich selbst zu feiern
-
-
-
-
- Ich bin fest davon überzeugt, dass jede Frau wunderschön ist — genau so, wie sie ist.
- Ein Boudoir-Shooting ist so viel mehr als nur Fotos. Es ist eine Reise zu dir selbst,
- ein Moment, in dem du dich fallen lassen und deine eigene Schönheit neu entdecken kannst.
-
-
- In meinem Studio schaffe ich einen geschützten Raum, in dem du dich wohl und sicher fühlst.
- Mit viel Einfühlungsvermögen und professioneller Anleitung entstehen Bilder,
- die deine einzigartige Persönlichkeit und Stärke zeigen.
-
-
-
- — Die Fotografin
-
-
- Mehr erfahren
-
-
-
-
-
-
- )
-}
diff --git a/src/components/sections/BlogPreview.tsx b/src/components/sections/BlogPreview.tsx
deleted file mode 100644
index 9a48bc7..0000000
--- a/src/components/sections/BlogPreview.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { ScrollReveal } from "@/components/ScrollReveal"
-
-export function BlogPreview() {
- // Static placeholders until real posts are seeded
- const posts = [
- {
- title: "Warum jede Frau ein Boudoir-Shooting erleben sollte",
- date: "15. Februar 2026",
- excerpt: "Ein Boudoir-Shooting ist weit mehr als schoene Fotos — es ist eine Reise der Selbstentdeckung und ein kraftvolles Statement der Selbstliebe.",
- },
- {
- title: "Behind the Scenes: So entsteht die perfekte Atmosphaere",
- date: "10. Februar 2026",
- excerpt: "Von der Musikauswahl bis zur Lichtgestaltung — ein Blick hinter die Kulissen meines Studios und wie ich den perfekten Rahmen schaffe.",
- },
- {
- title: "5 Tipps fuer dein erstes Boudoir-Shooting",
- date: "5. Februar 2026",
- excerpt: "Du ueberlegst, ein Boudoir-Shooting zu machen? Hier sind meine besten Tipps, damit du dich optimal vorbereitest und den Moment geniesst.",
- },
- ]
-
- return (
-
-
-
-
-
Journal
-
- Gedanken & Geschichten
-
-
-
-
-
-
- {posts.map((post, i) => (
-
-
- {/* Image placeholder */}
-
-
- {post.date}
-
-
- {post.title}
-
-
- {post.excerpt}
-
-
-
- ))}
-
-
-
-
-
- )
-}
diff --git a/src/components/sections/GalleryPreview.tsx b/src/components/sections/GalleryPreview.tsx
deleted file mode 100644
index 426796d..0000000
--- a/src/components/sections/GalleryPreview.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { ScrollReveal } from "@/components/ScrollReveal"
-
-export function GalleryPreview() {
- // Placeholder categories for the asymmetric grid
- const images = [
- { category: "Klassisch" },
- { category: "Elegant" },
- { category: "Artistisch" },
- { category: "Natürlich" },
- { category: "Sinnlich" },
- { category: "Dramatisch" },
- { category: "Klassisch" },
- { category: "Elegant" },
- ]
-
- return (
-
-
-
-
-
Portfolio
-
- Momente der Selbstliebe
-
-
-
- Jedes Shooting erzählt eine einzigartige Geschichte von Stärke, Anmut und Selbstliebe.
-
-
-
-
-
- {/* Asymmetric grid */}
-
- {images.map((img, i) => (
-
- {/* Hover overlay */}
-
-
- {img.category}
-
-
-
- ))}
-
-
-
-
-
-
- )
-}
diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx
deleted file mode 100644
index 2d14211..0000000
--- a/src/components/sections/Hero.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-"use client"
-
-import { useEffect, useState } from "react"
-
-export function Hero() {
- const [visible, setVisible] = useState(false)
-
- useEffect(() => {
- const timer = setTimeout(() => setVisible(true), 100)
- return () => clearTimeout(timer)
- }, [])
-
- return (
-
- {/* Radial gradient overlays */}
-
-
-
-
- {/* Decorative line */}
-
-
-
-
- Sensual
-
-
- Moment
-
-
-
-
- Boudoir Photography · Dein Moment der Selbstliebe
-
-
-
-
-
- {/* Scroll indicator */}
-
-
- )
-}
diff --git a/src/components/sections/Packages.tsx b/src/components/sections/Packages.tsx
deleted file mode 100644
index 17992ca..0000000
--- a/src/components/sections/Packages.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import { ScrollReveal } from "@/components/ScrollReveal"
-
-interface Package {
- name: string
- price: string
- features: string[]
- featured?: boolean
-}
-
-const packages: Package[] = [
- {
- name: "Entdecken",
- price: "Ab 349 EUR",
- features: [
- "1-2 Stunden Shooting",
- "Professionelles Styling-Beratung",
- "10 bearbeitete Digitalbilder",
- "Private Online-Galerie",
- "Persoenliche Bildauswahl",
- ],
- },
- {
- name: "Erleben",
- price: "Ab 599 EUR",
- featured: true,
- features: [
- "2-3 Stunden Shooting",
- "Professionelles Hair & Make-up",
- "25 bearbeitete Digitalbilder",
- "Private Online-Galerie",
- "1 Fine-Art Print (30x40)",
- "Outfitwechsel inklusive",
- ],
- },
- {
- name: "Zelebrieren",
- price: "Ab 899 EUR",
- features: [
- "3-4 Stunden Shooting",
- "Professionelles Hair & Make-up",
- "Alle bearbeiteten Digitalbilder",
- "Premium Fine-Art Album",
- "3 Fine-Art Prints",
- "Champagner & Verwoehn-Paket",
- ],
- },
-]
-
-export function Packages() {
- return (
-
-
-
-
-
Investition in dich
-
- Pakete & Preise
-
-
-
-
-
-
- {packages.map((pkg, i) => (
-
-
- {pkg.featured && (
-
-
- Beliebtestes Paket
-
-
- )}
-
-
- {pkg.name}
-
-
- {pkg.price}
-
-
-
- {pkg.features.map((f, j) => (
- -
- ✓
- {f}
-
- ))}
-
-
-
-
-
- ))}
-
-
-
- )
-}
diff --git a/src/components/sections/Testimonials.tsx b/src/components/sections/Testimonials.tsx
deleted file mode 100644
index 19a0cb5..0000000
--- a/src/components/sections/Testimonials.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ScrollReveal } from "@/components/ScrollReveal"
-
-interface Testimonial {
- id?: number
- quote: string
- author: string
- role?: string
-}
-
-interface TestimonialsProps {
- testimonials: Testimonial[]
-}
-
-export function Testimonials({ testimonials }: TestimonialsProps) {
- if (!testimonials.length) return null
-
- return (
-
-
-
-
-
Erfahrungen
-
- Was meine Kundinnen sagen
-
-
-
-
-
-
- {testimonials.slice(0, 3).map((t, i) => (
-
-
- {/* Decorative quote mark */}
-
- “
-
-
- {t.quote}
-
-
-
- {t.author}
-
- {t.role && (
-
- {t.role}
-
- )}
-
-
-
- ))}
-
-
-
- )
-}