feat: create design for 3bear and drsquatch hero banner, create variants cta section

This commit is contained in:
Nam Doan 2025-12-17 11:12:32 +07:00
parent 9e0f6b0071
commit 7afffb3f99
14 changed files with 280 additions and 129 deletions

View File

@ -177,6 +177,35 @@
] ]
} }
}, },
{
"Hero": {
"config": {
"className": "h-[35rem]",
"ImageDisplayer": {
"config": {
"duration": 0,
"images": ["./banner-hero.webp"],
"links": ["/account"]
}
},
"left": [
{
"VtCtaBanner": {
"config": {
"variant": "3bear",
"className": "left-[120px] top-[80px]",
"tagText": "So einfach kann Frühstück sein mit unseren leckeren Overnight Oats.",
"titleText": "breakfast made easy.",
"buttonText": "Jetzt entdecken"
}
}
}
],
"center": [],
"right": []
}
}
},
{ {
"CartMismatchBanner": { "CartMismatchBanner": {
"config": { "config": {
@ -213,10 +242,26 @@
"emailInputClassName": "w-[300px] ml-8" "emailInputClassName": "w-[300px] ml-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"
}
], ],
"socialsClassName": "ml-8 mt-10", "socialsClassName": "ml-8 mt-10",
"className": "", "className": "",
@ -311,15 +356,22 @@
"config": { "config": {
"className": "content-container bg-[#003f31] w-full text text-[#11314E] flex items-center justify-between", "className": "content-container bg-[#003f31] w-full text text-[#11314E] flex items-center justify-between",
"leftClassName": "w-full", "leftClassName": "w-full",
"left": [ "left": [],
],
"center": [], "center": [],
"right": [ "right": [
{ {
"VtFooterBottom": { "VtFooterBottom": {
"config": { "config": {
"className": " mr-[80px]", "className": " mr-[80px]",
"icons": ["Mastercard", "PayPal", "Visa", "Mastercard","Mastercard","Mastercard","Mastercard"] "icons": [
"Mastercard",
"PayPal",
"Visa",
"Mastercard",
"Mastercard",
"Mastercard",
"Mastercard"
]
} }
} }
} }

View File

@ -130,6 +130,44 @@
] ]
} }
}, },
{
"Hero": {
"config": {
"className": "h-[35rem]",
"ImageDisplayer": {
"config": {
"duration": 0,
"images": [
"./drsquatch-banner.jpg"
],
"links": [
"/account"
]
}
},
"left": [
{
"VtCtaBanner": {
"config": {
"variant": "default",
"className": "left-[120px] top-[120px] bg-transparent border-none text-white text-center",
"tagTextClassName": "text-white bg-transparent",
"titleTextClassName": "text-white font-bold leading-normal text-[30px]",
"descriptionTextClassName": "text-white text-[1rem] w-[300px] ml-[6.6rem]",
"buttonTextClassName": "text-white bg-orange-500 w-[300px]",
"tagText": "ALL NEW!",
"titleText": "LUMBERJACK LODGE",
"descriptionText": "Step into the lodge with the warm, sweet scent of maple and vanilla.",
"buttonText": "SHOP NOW"
}
}
}
],
"center": [],
"right": []
}
}
},
{ {
"CartMismatchBanner": { "CartMismatchBanner": {
"config": { "config": {
@ -277,10 +315,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"
}
] ]
} }
} }

View File

@ -138,14 +138,7 @@
"Hero": { "Hero": {
"config": { "config": {
"variant": "default", "variant": "default",
"className": "bg-custom-gradient", "className": "bg-custom-gradient"
"ImageDisplayer": {
"config": {
"duration": 20,
"images": ["./banner-hero.webp", "./banner-hero.webp"],
"links": []
}
}
} }
} }
}, },

View File

@ -179,29 +179,33 @@
{ {
"Hero": { "Hero": {
"config": { "config": {
"variant": "default",
"className": "h-[35rem]", "className": "h-[35rem]",
"ImageDisplayer": { "ImageDisplayer": {
"config": { "config": {
"duration": 1, "duration": 0,
"images": [ "images": [
"./banner-hero.webp", "./banner-hero.webp",
"./banner-hero.webp" "./banner-hero-1.webp",
"./banner-hero-2.webp"
], ],
"links": [] "links": ["/", "/account", "/product"]
} }
}, },
"left":[{ "left": [
{
"VtCtaBanner": { "VtCtaBanner": {
"config": { "config": {
"className": "ml-[120px]", "variant": "default",
"className": "left-[120px] top-[80px]",
"titleTextClassName": "leading-normal",
"tagText": "Besonders Aktion", "tagText": "Besonders Aktion",
"titleText": "Willkommen in Vibentec IT Shop", "titleText": "Willkommen in Vibentec IT Shop",
"descriptionText": "Insert the accordion description here. It would look better as two lines of text or more.", "descriptionText": "Insert the accordion description here. It would look better as two lines of text or more.",
"buttonText": "Zum Einkaufen" "buttonText": "Zum Einkaufen"
} }
} }
}], }
],
"center": [], "center": [],
"right": [] "right": []
} }

BIN
public/drsquatch-banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

View File

@ -9,14 +9,14 @@ export default function HeroDefault({ node, context }: { node: LayoutComponentDe
if (imageDisplayer) { if (imageDisplayer) {
return ( return (
<div className="absolute inset-0 z-10 w-full h-full"> <div className="absolute inset-0 z-auto w-full h-full">
<VtCarousel nodes={{ config: imageDisplayer.config }} context={context} /> <VtCarousel nodes={{ config: imageDisplayer.config }} context={context} />
</div> </div>
) )
} }
return ( return (
<div className="absolute inset-0 z-10 flex flex-col justify-center items-center text-center small:p-32 gap-6"> <div className="absolute inset-0 z-auto flex flex-col justify-center items-center text-center small:p-32 gap-6">
<span> <span>
<Heading <Heading
level="h1" level="h1"

View File

@ -1,28 +0,0 @@
import { Github } from "@medusajs/icons"
import { Button, clx, Heading } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { VtCarousel } from "../vt-carousel"
interface Props {
node: LayoutComponentDefinition
context: LayoutContext
}
const HeroVibentec = ({ node, context }: Props) => {
const props = node.config ?? {}
const imageDisplayer = props.ImageDisplayer
return (
<div className={clx(props.className)}>
{imageDisplayer ? (
<VtCarousel nodes={{ config: imageDisplayer.config }} context={context} />
) : (
<VtCarousel nodes={node} context={context} />
)}
</div>
)
}
export default HeroVibentec

View File

@ -3,14 +3,9 @@ import {
LayoutContext, LayoutContext,
} from "vibentec/component-map" } from "vibentec/component-map"
import { clx } from "@medusajs/ui" import { clx } from "@medusajs/ui"
import BannerHeroVibentec from "../hero/hero-vibentec" import BannerHero from "./banner-hero"
import BannerHeroDefault from "../hero/hero-default"
import { DynamicLayoutRenderer } from "vibentec/renderer" import { DynamicLayoutRenderer } from "vibentec/renderer"
interface BannerProps {
variant: "vibentec" | "default"
className?: string
}
export default async function Hero({ export default async function Hero({
nodes, nodes,
context, context,
@ -18,7 +13,7 @@ export default async function Hero({
nodes: LayoutComponentDefinition nodes: LayoutComponentDefinition
context: LayoutContext context: LayoutContext
}) { }) {
const props = (nodes.config as BannerProps) ?? {} const props = nodes.config ?? {}
const left = nodes.config?.left ?? [] const left = nodes.config?.left ?? []
const center = nodes.config?.center ?? [] const center = nodes.config?.center ?? []
const right = nodes.config?.right ?? [] const right = nodes.config?.right ?? []
@ -26,29 +21,21 @@ export default async function Hero({
"min-h-[30rem] w-full border-b border-ui-border-base relative", "min-h-[30rem] w-full border-b border-ui-border-base relative",
props.className props.className
) )
if (!props.variant) return null
const variants = {
vibentec: BannerHeroVibentec,
default: BannerHeroDefault,
}
const Component = variants[props.variant]
return ( return (
<div className={heroClassName}> <div className={heroClassName}>
<Component node={nodes} context={context} /> <BannerHero node={nodes} context={context} />
<div className="absolute inset-0 z-20 w-full h-full"> <div className="absolute z-20">
<nav className="content-container txt-xsmall-plus flex items-center justify-between w-full h-full text-small-regular"> <nav className="content-container txt-xsmall-plus flex items-center justify-between text-small-regular">
<div className="flex items-center gap-x-4 w-full h-full"> <div className="flex items-center gap-x-4">
{left && <DynamicLayoutRenderer nodes={left} context={context} />} {left && <DynamicLayoutRenderer nodes={left} context={context} />}
</div> </div>
<div className="flex items-center gap-x-4 w-full h-full"> <div className="flex items-center gap-x-4">
{center && ( {center && (
<DynamicLayoutRenderer nodes={center} context={context} /> <DynamicLayoutRenderer nodes={center} context={context} />
)} )}
</div> </div>
<div className="flex items-center gap-x-4 w-full h-full justify-end"> <div className="flex items-center gap-x-4 justify-end">
{right && <DynamicLayoutRenderer nodes={right} context={context} />} {right && <DynamicLayoutRenderer nodes={right} context={context} />}
</div> </div>
</nav> </nav>

View File

@ -44,6 +44,7 @@
display: grid; display: grid;
position: absolute; position: absolute;
top: 0; top: 0;
z-index: 1000;
width: 100%; width: 100%;
height: 100%; height: 100%;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
@ -57,6 +58,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
z-index: 1000;
} }
.embla__button { .embla__button {
--text-high-contrast-rgb-value: 49, 49, 49; --text-high-contrast-rgb-value: 49, 49, 49;
@ -74,7 +76,7 @@
margin: 0; margin: 0;
width: 3.6rem; width: 3.6rem;
height: 3.6rem; height: 3.6rem;
z-index: 1; z-index: 1001;
border-radius: 50%; border-radius: 50%;
color: var(--text-body); color: var(--text-body);
display: flex; display: flex;
@ -98,6 +100,7 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
z-index: 1000;
} }
.embla__dot { .embla__dot {
--text-high-contrast-rgb-value: 49, 49, 49; --text-high-contrast-rgb-value: 49, 49, 49;

View File

@ -9,6 +9,8 @@ import Autoplay from "embla-carousel-autoplay"
import { useMemo } from "react" import { useMemo } from "react"
import { DotButton, useDotButton } from "./carousel-dot-button" import { DotButton, useDotButton } from "./carousel-dot-button"
import { NextButton, PrevButton, usePrevNextButtons } from "./carousel-arrow-button" import { NextButton, PrevButton, usePrevNextButtons } from "./carousel-arrow-button"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
export function VtCarousel({ export function VtCarousel({
nodes, nodes,
context, context,
@ -18,13 +20,21 @@ export function VtCarousel({
}) { }) {
const props = nodes.config ?? {} const props = nodes.config ?? {}
const { options } = props as any const { options } = props as any
const images: string[] = (props as any).images ?? (props as any).slides ?? [] const images: string[] = props.images ?? props.slides ?? []
const links: (string | undefined)[] = (props as any).links ?? [] const links: (string | undefined)[] = props.links ?? []
const durationSeconds: number = props.duration ?? 4 const durationSeconds: number = props.duration ?? 4
const showControls = images.length > 1
const plugins = useMemo(() => [ const plugins = useMemo(() => {
Autoplay({ delay: Math.max(1, durationSeconds) * 1000, stopOnInteraction: false, stopOnMouseEnter: true, }) if (!durationSeconds || durationSeconds <= 0) return []
], [durationSeconds]) return [
Autoplay({
delay: durationSeconds * 1000,
stopOnInteraction: false,
stopOnMouseEnter: true,
}),
]
}, [durationSeconds])
const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins) const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins)
const { selectedIndex, scrollSnaps, onDotButtonClick } = const { selectedIndex, scrollSnaps, onDotButtonClick } =
@ -41,12 +51,12 @@ export function VtCarousel({
<div className={styles["embla__viewport"]} ref={emblaRef}> <div className={styles["embla__viewport"]} ref={emblaRef}>
<div className={styles["embla__container"]}> <div className={styles["embla__container"]}>
{images && images.map((src: string, index: number) => ( {images && images.map((src: string, index: number) => (
<div className={styles["embla__slide"]} key={index}> <div className={styles["embla__slide"]} key={index + src}>
<div className={styles["embla__slide__number"]}> <div className={styles["embla__slide__number"]}>
{links[index] ? ( {links[index] ? (
<a href={links[index]}> <LocalizedClientLink href={links[index]}>
<img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} /> <img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} />
</a> </LocalizedClientLink>
) : ( ) : (
<img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} /> <img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} />
)} )}
@ -56,6 +66,7 @@ export function VtCarousel({
</div> </div>
</div> </div>
{showControls && (
<div className={styles["embla__controls"]}> <div className={styles["embla__controls"]}>
<div className={styles["embla__buttons"]}> <div className={styles["embla__buttons"]}>
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} /> <PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
@ -75,6 +86,7 @@ export function VtCarousel({
))} ))}
</div> </div>
</div> </div>
)}
</section> </section>
) )
} }

View File

@ -0,0 +1,35 @@
"use client"
import { Button, clx } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export function BearCtaBanner({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
return (
<div className={clx(
"relative w-full p-12 flex flex-col items-start justify-center text-left",
props.className
)}>
<p className="text-[#0D382E] text-lg font-medium mb-2">
{props.tagText ?? "So einfach kann Frühstück sein mit unseren leckeren Overnight Oats."}
</p>
<h1 className="text-[#0D382E] font-black leading-tight text-[64px] mb-8 tracking-tight">
{props.titleText ?? "breakfast made easy."}
</h1>
<Button className="inline-flex items-center justify-center bg-[#FCEE56] hover:bg-[#FCEE56]/90 text-[#0D382E] px-8 py-3 rounded-full font-bold text-lg shadow-none border-none">
{props.buttonText ?? "Jetzt entdecken"}
</Button>
</div>
)
}

View File

@ -0,0 +1,52 @@
"use client"
import { Button, clx } from "@medusajs/ui"
import { ChevronRightMini } from "@medusajs/icons"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export function DefaultCtaBanner({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
return (
<div className={clx(
"relative w-[544px] bg-white rounded-[24px] border border-[#E6EFFC] shadow-[0_12px_40px_rgba(17,49,78,0.10)] p-6",
props.className
)}>
<div className={clx(
"inline-flex items-center rounded-full bg-[#FCE9DA] text-[#E68445] px-3 py-1 text-sm font-medium",
props.tagTextClassName
)}>
{props.tagText}
</div>
<h1 className={clx(
"mt-4 text-[#11314E] font-semibold leading-normal text-[56px]",
props.titleTextClassName
)}>
{props.titleText}
</h1>
<p className={clx(
"mt-5 text-[#285A86] text-[16px] sm:text-xl opacity-80",
props.descriptionTextClassName
)}>
{props.descriptionText}
</p>
<Button className={clx(
"mt-8 inline-flex items-center gap-2 bg-[#0F2740] hover:bg-[#173551] text-white px-6 py-3 rounded-[12px] shadow-md",
props.buttonTextClassName
)}>
{props.buttonText}
<ChevronRightMini />
</Button>
</div>
)
}

View File

@ -1,10 +1,10 @@
"use client" "use client"
import { Button, clx } from "@medusajs/ui"
import { ChevronRightMini } from "@medusajs/icons"
import { import {
LayoutComponentDefinition, LayoutComponentDefinition,
LayoutContext, LayoutContext,
} from "@vibentec/component-map" } from "@vibentec/component-map"
import { DefaultCtaBanner } from "./default-cta"
import { BearCtaBanner } from "./bear-cta"
export function VtCtaBanner({ export function VtCtaBanner({
nodes, nodes,
@ -14,27 +14,14 @@ export function VtCtaBanner({
context: LayoutContext context: LayoutContext
}) { }) {
const props = nodes.config ?? {} const props = nodes.config ?? {}
return ( const variant = props.variant ?? "default"
<div className={clx(
"relative w-[544px] bg-white rounded-[24px] border border-[#E6EFFC] shadow-[0_12px_40px_rgba(17,49,78,0.10)] p-6",
props.className
)}>
<div className="inline-flex items-center rounded-full bg-[#FCE9DA] text-[#E68445] px-3 py-1 text-sm font-medium">
{props.tagText}
</div>
<h1 className="mt-4 text-[#11314E] font-semibold leading-tight text-[56px]"> const variants: Record<string, any> = {
{props.titleText} default: DefaultCtaBanner,
</h1> "3bear": BearCtaBanner,
}
<p className="mt-5 text-[#285A86] text-[16px] sm:text-xl opacity-80">
{props.descriptionText} const Component = variants[variant] || DefaultCtaBanner
</p>
return <Component nodes={nodes} context={context} />
<Button className="mt-8 inline-flex items-center gap-2 bg-[#0F2740] hover:bg-[#173551] text-white px-6 py-3 rounded-[12px] shadow-md">
{props.buttonText}
<ChevronRightMini />
</Button>
</div>
)
} }

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.namVibentec; const fileName = jsonFileNames.namStarter;
export async function loadDesignConfig() { export async function loadDesignConfig() {
const filePath = path.join(process.cwd(), "config", fileName) const filePath = path.join(process.cwd(), "config", fileName)