Compare commits

..

4 Commits

9 changed files with 782 additions and 32 deletions

View File

@ -474,6 +474,107 @@
} }
} }
}, },
{
"VtBrand": {
"config": {
"className": "w-full py-12 bg-[#CFECD9]",
"innerClassName": "content-container flex flex-col items-center",
"title": "Trusted By",
"titleClassName": "text-[#003F31] text-[20px] font-semibold mb-8",
"brandsClassName": "flex w-full items-center justify-between gap-12",
"items": [
{
"label": "Men'sHealth",
"containerClassName": "",
"className": "text-[#003F31] text-[36px] font-semibold italic"
},
{
"label": "GQ",
"containerClassName": "",
"className": "text-[#003F31] text-[44px] font-black tracking-tight"
},
{
"label": "BIRCHBOX",
"containerClassName": "",
"className": "text-[#003F31] text-[36px] font-semibold tracking-[0.2em]"
}
]
}
}
},
{
"VtFeedbackCard": {
"config": {
"className": "content-container py-16 bg-[#CFECD9] mt-16",
"title": "Der Hafer-Hype ist real. Finden nicht nur 100.000+ zufriedene 3Bears Fans.",
"gridClassName": "grid grid-cols-1 small:grid-cols-2 xl:grid-cols-4 gap-6",
"cardClassName": "rounded-2xl overflow-hidden",
"imageClassName": "w-full h-[260px] object-cover",
"contentClassName": "p-6",
"nameClassName": "text-[#003F31] text-[20px] font-bold",
"subtitleClassName": "mt-1 text-[#003f31b3] text-[14px]",
"quoteClassName": "mt-4 text-[#003F31] text-[16px]",
"ctaClassName": "mt-6 inline-flex items-center justify-center bg-[#FCEE56] text-[#0D382E] px-6 py-2 rounded-full font-bold",
"items": [
{
"imageSrc": "/overnight-oat.webp",
"name": "Harry Kane",
"subtitle": "Profißballer, Kapitän engl. Nationalmannschaft, Stürmer FC Bayern",
"quote": "Als Sportler ist das Frühstück die wichtigste Mahlzeit für mich, und natürlich achte ich sehr darauf, was ich esse. Als ich 3Bears entdeckt habe, hat mich nachhaltig beeindruckt, dass die Haferflocken auf ein neues Level heben.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Sally Özcan",
"subtitle": "Foodcreatorin & Unternehmerin",
"quote": "Ich liebe Frühstück, weil es für mich der Start in einen guten Tag ist, mit meiner Familie, meinem Team oder unterwegs. Ich mag Produkte, die einfach einen Sinn ergeben, natürlich, lecker und ohne Schnickschnack. Genau das ist 3Bears für mich."
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Sarah Harrison",
"subtitle": "Unternehmerin & Influencerin",
"quote": "3Bears teilt meine Leidenschaft für hochwertige Lebensmittel, die nicht nur mega lecker, sondern auch vollwertig sind. Deswegen war ich so begeistert von der Idee, gemeinsam ein Granola zu entwickeln.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
},
{
"imageSrc": "/overnight-oat.webp",
"name": "Hendrik Pfeiffer",
"subtitle": "Profi-Läufer & German Champion",
"quote": "Als Profisportler spielt meine bewusste Ernährung eine absolute Schlüsselrolle, um vorne mitmischen zu können. Die Produkte von 3Bears passen dabei wie die Faust aufs Auge.",
"cta": {
"label": "Mehr erfahren",
"href": "/"
}
}
]
}
}
},
{
"VtSubcription": {
"config": {
"className": "content-container py-12 flex justify-center",
"cardClassName": "rounded-2xl overflow-hidden bg-[#CFECD9] w-[800px] p-10",
"title": "10% für dich!",
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
"description": true,
"descriptionPrefix": "Melde dich jetzt zum 3Bears Newsletter an und sichere dir",
"descriptionHighlight": "10% Rabatt auf deinen nächsten Einkauf!",
"descriptionSuffix": "",
"subtext": "Deinen Rabattcode bekommst du von uns per Mail.",
"firstName": { "placeholder": "Vorname" },
"email": { "placeholder": "E-Mail-Adresse" },
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
"cta": { "label": "Anmelden" }
}
}
},
{ {
"FreeShippingPriceNudge": { "FreeShippingPriceNudge": {
"config": { "config": {

View File

@ -131,7 +131,9 @@
] ]
} }
}, },
{ "PropsChildren": {} }, {
"PropsChildren": {}
},
{ {
"Footer": { "Footer": {
"config": { "config": {
@ -147,12 +149,30 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white gap-8", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white gap-8",
"itemClassName": "text-[14px] font-[400] mt-3", "itemClassName": "text-[14px] font-[400] mt-3",
"items": [ "items": [
{ "text": "FAQ", "href": "/" }, {
{ "text": "Track my order", "href": "/categories/shoes" }, "text": "FAQ",
{ "text": "Placeholder", "href": "/categories/accessories" }, "href": "/"
{ "text": "Placeholder", "href": "/categories/accessories" }, },
{ "text": "Placeholder", "href": "/categories/accessories" }, {
{ "text": "Placeholder", "href": "/categories/accessories" } "text": "Track my order",
"href": "/categories/shoes"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -164,13 +184,34 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] flex items-center mt-3", "itemClassName": "text-[14px] font-[400] flex items-center mt-3",
"items": [ "items": [
{ "text": "Twitter", "href": "/" }, {
{ "text": "Facebook", "href": "/categories/shoes" }, "text": "Twitter",
{ "text": "Pinterest", "href": "/categories/accessories" }, "href": "/"
{ "text": "Placeholder", "href": "/categories/accessories" }, },
{ "text": "Placeholder", "href": "/categories/accessories" }, {
{ "text": "Placeholder", "href": "/categories/accessories" }, "text": "Facebook",
{ "text": "Placeholder", "href": "/categories/accessories" } "href": "/categories/shoes"
},
{
"text": "Pinterest",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -182,9 +223,18 @@
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white", "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] w-[200px] mt-3", "itemClassName": "text-[14px] font-[400] w-[200px] mt-3",
"items": [ "items": [
{ "text": "The Squatch Difference", "href": "/" }, {
{ "text": "Why Natural Products", "href": "/categories/shoes" }, "text": "The Squatch Difference",
{ "text": "No Harmful Ingredients", "href": "/categories/accessories" } "href": "/"
},
{
"text": "Why Natural Products",
"href": "/categories/shoes"
},
{
"text": "No Harmful Ingredients",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -214,10 +264,26 @@
"buttonClassName": "bg-[#C4622C] w-[90px]", "buttonClassName": "bg-[#C4622C] w-[90px]",
"socialsClassName": "mt-4 gap-8", "socialsClassName": "mt-4 gap-8",
"socials": [ "socials": [
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, {
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, "icon": "Twitter",
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }, "href": "/",
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" } "className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
},
{
"icon": "Twitter",
"href": "/",
"className": "w-5 h-5 text-white"
}
] ]
} }
} }
@ -233,9 +299,18 @@
"text": "©2025 Vibentec IT. All rights reserved", "text": "©2025 Vibentec IT. All rights reserved",
"linksClassName": "flex items-center text-orange-500 mt-2 pl-2", "linksClassName": "flex items-center text-orange-500 mt-2 pl-2",
"links": [ "links": [
{ "label": "Privacy Policy", "href": "/" }, {
{ "label": "Terms of Service", "href": "/categories/shoes" }, "label": "Privacy Policy",
{ "label": "Cookie Policy", "href": "/categories/accessories" } "href": "/"
},
{
"label": "Terms of Service",
"href": "/categories/shoes"
},
{
"label": "Cookie Policy",
"href": "/categories/accessories"
}
] ]
} }
} }
@ -250,8 +325,12 @@
"ImageDisplayer": { "ImageDisplayer": {
"config": { "config": {
"duration": 0, "duration": 0,
"images": ["./drsquatch-banner.jpg"], "images": [
"links": ["/account"] "./drsquatch-banner.jpg"
],
"links": [
"/account"
]
} }
}, },
"left": [ "left": [
@ -297,7 +376,10 @@
"container": "absolute z-[1] top-0 left-0 pt-4", "container": "absolute z-[1] top-0 left-0 pt-4",
"text": "uppercase px-4 py-2 bg-[#3B6F47] text-white" "text": "uppercase px-4 py-2 bg-[#3B6F47] text-white"
}, },
"thumbnail": { "className": "rounded-none h-[300px] shadow-none", "size": "full" }, "thumbnail": {
"className": "rounded-none h-[300px] shadow-none",
"size": "full"
},
"content": " flex flex-col flex-1", "content": " flex flex-col flex-1",
"title": "mt-2 text-[#1f3521] text-[22px] font-bold", "title": "mt-2 text-[#1f3521] text-[22px] font-bold",
"price": "mt-2 text-[#3B6F47] font-bold text-[20px] flex gap-3 flex-row-reverse justify-end", "price": "mt-2 text-[#3B6F47] font-bold text-[20px] flex gap-3 flex-row-reverse justify-end",
@ -348,14 +430,142 @@
} }
} }
}, },
{ "CartMismatchBanner": { "config": { "show": true } } }, {
{ "FreeShippingPriceNudge": { "config": { "variant": "popup" } } } "VtBrand": {
], "config": {
"Product": [ "className": "w-full py-12 bg-[#CFECD9]",
{ "VtFeaturedProducts": { "config": { "title": "drsquatch-best-seller" } } } "innerClassName": "content-container flex flex-col items-center",
], "title": "",
"StorePage": [ "titleClassName": "text-[#1f3521] text-[20px] font-bold mb-8",
{ "VtFeaturedProducts": { "config": { "title": "drsquatch-best-seller" } } } "brandsClassName": "flex w-full items-center justify-between gap-12",
"items": [
{
"imageSrc": "/brand-logo.png",
"alt": "Men's Health",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
},
{
"imageSrc": "/brand-logo.png",
"alt": "GQ",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
},
{
"imageSrc": "/brand-logo.png",
"alt": "Birchbox",
"containerClassName": "",
"imageClassName": "h-[40px] object-contain"
}
]
}
}
},
{
"VtSubcription": {
"config": {
"className": "w-full",
"cardClassName": "overflow-hidden bg-[#F3EDE3] p-10",
"title": "SUBSCRIBE & SAVE",
"titleClassName": "text-[#003F31] text-[28px] font-bold text-center",
"description": true,
"policyLabel": "Ich habe die DSGVO gelesen und akzeptiere sie.",
"benefits": [
{
"icon": "🗓",
"title": "Ships Every 3 Months",
"description": "Customize your picks and scents, upgrade anytime, or hit snooze if you want. You're in control."
},
{
"icon": "🚚",
"title": "Free Delivery",
"description": "Subscribe once and relax. All your shipping expenses are covered by Squatch."
},
{
"icon": "⭐",
"title": "Exclusive Benefits",
"description": "Gain exclusive, subscriber-only benefits. Enjoy early access to new products and limited edition releases!"
}
],
"formClassName": "flex justify-center",
"cta": {
"label": "SUBSCRIBE & SAVE",
"className": "w-fit mt-12 px-[30px] h-[56px] rounded-full bg-orange-500 text-white font-bold"
}
}
}
},
{
"VtFeedback": {
"config": {
"title": "100,000+ Reviews From Squatchers",
"className": "content-container py-16",
"titleClassName": "text-[#1f3521] text-[28px] font-bold text-center mb-10",
"duration": 5,
"options": {
"loop": true
},
"itemClassName": "min-w-full px-6",
"starsClassName": "text-[#C4622C] text-xl leading-none",
"reviewTitleClassName": "text-[#1f3521] font-bold",
"reviewTextClassName": "text-[#1f3521]",
"authorClassName": "italic text-[#1f3521]",
"controls": "mt-6 flex items-center justify-center gap-4",
"items": [
{
"rating": 5,
"title": "Ah-freaking-amazing!",
"text": "So I just had my first shower with Dr. Squatch Cool Fresh Aloe. Holy sh*t this stuff is Ah-freaking-amazing! Talk about a life hack!",
"author": "Stephen B."
},
{
"rating": 5,
"title": "Best damn soap ever…period.",
"text": "Best Damn Soap I EVER bought! Super smooth on the skin, smells awesome, makes you feel good showering, and yes…the wife approves.",
"author": "Chris H."
},
{
"rating": 5,
"title": "Hilarious…products awesome too",
"text": "Ok…the Dr. Squatch commercials are just freakin hilarious…plus the products are awesome too! So yes, buy it now and subscribe to it!",
"author": "Mike C."
}
]
}
}
},
{
"CartMismatchBanner": {
"config": {
"show": true
}
}
},
{
"FreeShippingPriceNudge": {
"config": {
"variant": "popup"
}
}
}
],
"Product": [
{
"VtFeaturedProducts": {
"config": {
"title": "drsquatch-best-seller"
}
}
}
],
"StorePage": [
{
"VtFeaturedProducts": {
"config": {
"title": "drsquatch-best-seller"
}
}
}
] ]
} }
} }

BIN
public/brand-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,72 @@
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default async function VtBrand({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? "Trusted By"
const items = props.items ?? []
const classes = {
container: props.className ?? "w-full py-12 bg-[#CFECD9]",
inner: props.innerClassName ?? "content-container flex flex-col items-center",
title: props.titleClassName ?? "text-[#1f3521] text-[20px] font-bold mb-8",
brands: props.brandsClassName ?? "flex w-full items-center justify-between gap-12",
item: props.itemClassName ?? "opacity-90",
image: props.imageClassName ?? "h-[48px] w-auto object-contain",
label: props.labelClassName ?? "text-[#1f3521] text-[36px] font-semibold",
}
if (!items || items.length === 0) {
return null
}
const renderItem = (brand: any, idx: number) => {
const content = brand.imageSrc ? (
<img
src={brand.imageSrc}
alt={brand.alt ?? brand.label ?? `brand-${idx}`}
className={clx(classes.image, brand.imageClassName)}
/>
) : (
<span className={clx(classes.label, brand.className)}>
{brand.label ?? ""}
</span>
)
return brand.href ? (
<LocalizedClientLink
key={`brand-${idx}`}
href={brand.href}
className={clx(classes.item, brand.containerClassName)}
>
{content}
</LocalizedClientLink>
) : (
<div className={clx(classes.item, brand.containerClassName)} key={`brand-${idx}`}>
{content}
</div>
)
}
return (
<section className={classes.container}>
<div className={classes.inner}>
{title && <div className={classes.title}>{title}</div>}
<div className={classes.brands}>
{items.map((brand: any, idx: number) => renderItem(brand, idx))}
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,74 @@
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default async function VtFeedbackCard({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? ""
const items = props.items ?? []
const classes = {
container: props.className ?? "",
title: props.titleClassName ?? "text-[#003F31] text-[28px] font-semibold mb-10",
grid: props.gridClassName ?? "grid grid-cols-1 small:grid-cols-2 xl:grid-cols-4 gap-6",
card: props.cardClassName ?? "rounded-2xl overflow-hidden bg-[#CFECD9]",
image: props.imageClassName ?? "w-full h-[260px] object-cover",
content: props.contentClassName ?? "p-6",
name: props.nameClassName ?? "text-[#003F31] text-[20px] font-bold",
subtitle: props.subtitleClassName ?? "mt-1 text-[#003f31b3] text-[14px]",
quote: props.quoteClassName ?? "mt-4 text-[#003F31] text-[16px]",
cta: props.ctaClassName ?? "mt-6 inline-flex items-center justify-center bg-[#FCEE56] text-[#0D382E] px-6 py-2 rounded-full font-bold",
}
if (!items || items.length === 0) return null
const renderCard = (entry: any, idx: number) => {
const imageEl = entry.imageSrc ? (
<img
src={entry.imageSrc}
alt={entry.imageAlt ?? entry.name ?? `feedback-card-${idx}`}
className={classes.image}
/>
) : null
const ctaEl = entry.cta?.href ? (
<LocalizedClientLink href={entry.cta.href} className={clx(classes.cta, entry.cta?.className)}>
{entry.cta.label ?? "Mehr erfahren"}
</LocalizedClientLink>
) : entry.cta?.label ? (
<button className={clx(classes.cta, entry.cta?.className)}>{entry.cta.label}</button>
) : null
return (
<div className={clx(classes.card, entry.className)} key={`vt-feedback-card-${idx}`}>
{imageEl}
<div className={clx(classes.content)}>
{entry.name && <div className={clx(classes.name, entry.nameClassName)}>{entry.name}</div>}
{entry.subtitle && (
<div className={clx(classes.subtitle, entry.subtitleClassName)}>{entry.subtitle}</div>
)}
{entry.quote && <div className={clx(classes.quote, entry.quoteClassName)}>{entry.quote}</div>}
{ctaEl}
</div>
</div>
)
}
return (
<section className={classes.container}>
{title && <h2 className={classes.title}>{title}</h2>}
<div className={classes.grid}>{items.map((it: any, idx: number) => renderCard(it, idx))}</div>
</section>
)
}

View File

@ -0,0 +1,91 @@
"use client"
import useEmblaCarousel from "embla-carousel-react"
import Autoplay from "embla-carousel-autoplay"
import { useMemo } from "react"
import { clx } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { NextButton, PrevButton, usePrevNextButtons } from "@modules/layout/templates/vt-carousel/carousel-arrow-button"
export default function VtFeedback({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title: string = props.title ?? ""
const items = props.items ?? []
const durationSeconds: number = props.duration ?? 5
const options = props.options ?? { loop: true }
const plugins = useMemo(() => {
if (!durationSeconds || durationSeconds <= 0) return []
return [
Autoplay({
delay: durationSeconds * 1000,
stopOnInteraction: false,
stopOnMouseEnter: true,
}),
]
}, [durationSeconds])
const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins)
const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = usePrevNextButtons(emblaApi)
const classes = {
container: props.className ?? "content-container py-16",
title: props.titleClassName ?? "text-[#1f3521] text-[28px] font-bold text-center mb-10",
viewport: "relative overflow-hidden",
containerInner: "flex",
slide: props.itemClassName ?? "min-w-full px-6",
slideInner: "flex flex-col items-center text-center gap-3",
stars: props.starsClassName ?? "text-[#C4622C] text-xl leading-none",
reviewTitle: props.reviewTitleClassName ?? "text-[#1f3521] font-bold",
reviewText: props.reviewTextClassName ?? "text-[#1f3521]",
author: props.authorClassName ?? "italic text-[#1f3521]",
controls: props.controls,
}
if (!items || items.length === 0) return null
const showControls = items.length > 1 && classes.controls
const renderStars = (rating?: number) => {
const count = Math.max(0, Math.min(5, Math.round(rating ?? 5)))
return "★★★★★".slice(0, count)
}
return (
<section className={classes.container}>
{title && <h2 className={classes.title}>{title}</h2>}
<div className={classes.viewport} ref={emblaRef}>
<div className={classes.containerInner}>
{items.map((it: any, idx: number) => (
<div className={classes.slide} key={`feedback-${idx}`}>
<div className={classes.slideInner}>
<div className={classes.stars}>{renderStars(it.rating)}</div>
{it.title && <div className={classes.reviewTitle}>{it.title}</div>}
{it.text && <div className={classes.reviewText}>{it.text}</div>}
{it.author && <div className={classes.author}>{it.author}</div>}
</div>
</div>
))}
</div>
{showControls && (
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 flex items-center justify-between px-4">
<div className="pointer-events-auto">
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
</div>
<div className="pointer-events-auto">
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</div>
</div>
)}
</div>
</section>
)
}

View File

@ -0,0 +1,193 @@
"use client"
import { Button } from "@medusajs/ui"
import { clx } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import React, { useState } from "react"
interface BenefitItem {
icon?: string
imgSrc?: string
title?: string
description?: string
className?: string
iconClassName?: string
titleClassName?: string
descriptionClassName?: string
}
export default function VtSubcription({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const [firstName, setFirstName] = useState("")
const [email, setEmail] = useState("")
const [accepted, setAccepted] = useState(false)
const [submitted, setSubmitted] = useState(false)
const classes = {
container: props.className ?? "content-container",
card: props.cardClassName ?? "rounded-2xl bg-[#CFECD9] p-8 small:p-12",
title:
props.titleClassName ??
"text-[#003F31] text-[28px] font-bold text-center",
description:
props.descriptionClassName ?? "mt-2 text-center text-[#003F31]",
highlight: props.highlightClassName ?? "font-bold",
form: props.formClassName ?? "mt-8 flex flex-col gap-6",
fields: props.fieldsClassName ?? "grid grid-cols-1 small:grid-cols-2 gap-4",
input:
props.inputClassName ??
"h-[52px] rounded-md border border-[#003F31]/40 px-4 bg-transparent text-[#003F31]",
checkboxRow: props.checkboxRowClassName ?? "flex items-center gap-3",
checkbox:
props.checkboxClassName ??
"w-5 h-5 rounded-md border border-[#003F31]/60",
checkboxLabel: props.checkboxLabelClassName ?? "text-[#003F31] text-[16px]",
subtextClass: props.subtextClassName ?? "text-[#003F31]",
submit:
props.submitClassName ?? "",
success: props.successClassName ?? "mt-4 text-center text-[#003F31]",
benefits:
props.benefitsClassName ??
"mt-8 grid grid-cols-1 small:grid-cols-3 gap-8",
benefitItem:
props.benefitItemClassName ??
"flex flex-col items-center text-center gap-3",
benefitIcon:
props.benefitIconClassName ??
"w-12 h-12 rounded-full bg-[#003F31] text-white flex items-center justify-center",
benefitTitle: props.benefitTitleClassName ?? "text-[#003F31] font-semibold",
benefitDesc: props.benefitDescClassName ?? "text-[#003F31] opacity-80",
}
const submitConfig = props.cta ?? {}
const policyLabel: string =
props.policyLabel ?? "Ich habe die DSGVO gelesen und akzeptiere sie."
const firstNameField = props.firstName ?? null
const emailField = props.email ?? null
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!accepted) return
setSubmitted(true)
console.log("subscription_submit", { firstName, email, accepted })
}
return (
<section className={classes.container}>
<div className={classes.card}>
{props.title && <h2 className={classes.title}>{props.title}</h2>}
{props.description && (
<p className={classes.description}>
{props.descriptionPrefix}{" "}
<span className={classes.highlight}>
{props.descriptionHighlight}
</span>{" "}
{props.descriptionSuffix}
</p>
)}
{Array.isArray(props.benefits) && props.benefits.length > 0 && (
<div className={classes.benefits}>
{props.benefits.map((b: BenefitItem, i: number) => (
<div
key={`benefit-${i}`}
className={clx(classes.benefitItem, b.className)}
>
{b.imgSrc ? (
<img
src={b.imgSrc}
alt={b.title ?? `benefit-${i}`}
className={clx(classes.benefitIcon, b.iconClassName)}
/>
) : (
<div
className={clx(classes.benefitIcon, b.iconClassName)}
aria-hidden="true"
>
{b.icon ?? ""}
</div>
)}
{b.title && (
<div className={clx(classes.benefitTitle, b.titleClassName)}>
{b.title}
</div>
)}
{b.description && (
<div
className={clx(classes.benefitDesc, b.descriptionClassName)}
>
{b.description}
</div>
)}
</div>
))}
</div>
)}
{props.subtext && (
<p
className={clx(
"mt-2 text-center", classes?.subtextClass ?? "text-[#003F31]"
)}
>
{props.subtext}
</p>
)}
<form className={classes.form} onSubmit={onSubmit}>
<div className={classes.fields}>
{firstNameField && (
<input
type="text"
placeholder={firstNameField.placeholder ?? "Vorname"}
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
className={clx(classes.input, firstNameField.className)}
/>
)}
{emailField && (
<input
type="email"
placeholder={emailField.placeholder ?? "E-Mail-Adresse"}
value={email}
onChange={(e) => setEmail(e.target.value)}
className={clx(classes.input, emailField.className)}
required
/>
)}
</div>
{props.newCheckboxRowClassName && (
<label className={classes.checkboxRow}>
<input
type="checkbox"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
className={classes.checkbox}
/>
<span className={classes.checkboxLabel}>{policyLabel}</span>
</label>
)}
<button
type="submit"
className={clx(classes.submit, submitConfig.className)}
>
{submitConfig.label ?? "Anmelden"}
</button>
</form>
{submitted && (
<div className={classes.success}>
{props.successMessage ??
"Danke! Prüfe deine E-Mails für den Rabattcode."}
</div>
)}
</div>
</section>
)
}

View File

@ -27,6 +27,10 @@ import { VtCarousel } from "@modules/layout/templates/vt-carousel"
import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner" import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner"
import VtFeaturedProducts from "@modules/home/components/vt-featured-products" import VtFeaturedProducts from "@modules/home/components/vt-featured-products"
import VtCategoryHighlight from "@modules/home/components/vt-category-highlight" import VtCategoryHighlight from "@modules/home/components/vt-category-highlight"
import VtBrand from "@modules/home/components/vt-brand"
import VtFeedback from "@modules/home/components/vt-feedback"
import VtFeedbackCard from "@modules/home/components/vt-feedback-card"
import VtSubcription from "@modules/home/components/vt-subcription"
type ComponentConfig = Record<string, any> type ComponentConfig = Record<string, any>
@ -103,6 +107,10 @@ export const componentMap: Record<string, ComponentRenderer> = {
ImageDisplayer: nodesContextRenderer(VtCarousel), ImageDisplayer: nodesContextRenderer(VtCarousel),
VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts), VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts),
VtCategoryHighlight: nodesContextRenderer(VtCategoryHighlight), VtCategoryHighlight: nodesContextRenderer(VtCategoryHighlight),
VtBrand: nodesContextRenderer(VtBrand),
VtFeedback: nodesContextRenderer(VtFeedback),
VtFeedbackCard: nodesContextRenderer(VtFeedbackCard),
VtSubcription: nodesContextRenderer(VtSubcription),
} }
export type ComponentName = keyof typeof componentMap export type ComponentName = keyof typeof componentMap

View File

@ -2,7 +2,7 @@ import fs from "fs"
import path from "path" import path from "path"
import { jsonFileNames } from "./devJsonFileNames" import { jsonFileNames } from "./devJsonFileNames"
const fileName = jsonFileNames.nam3Bear const fileName = jsonFileNames.namDrsquatch
async function readDesignFile() { async function readDesignFile() {
const filePath = path.join(process.cwd(), "config", fileName) const filePath = path.join(process.cwd(), "config", fileName)
@ -10,6 +10,7 @@ async function readDesignFile() {
return JSON.parse(fileData) return JSON.parse(fileData)
} }
export async function loadLayoutConfig() { export async function loadLayoutConfig() {
const config = await readDesignFile() const config = await readDesignFile()
if (Array.isArray(config)) return config if (Array.isArray(config)) return config