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": {
"config": {
@ -213,10 +242,26 @@
"emailInputClassName": "w-[300px] ml-8"
},
"socials": [
{ "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" },
{ "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"
},
{
"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",
"className": "",
@ -311,15 +356,22 @@
"config": {
"className": "content-container bg-[#003f31] w-full text text-[#11314E] flex items-center justify-between",
"leftClassName": "w-full",
"left": [
],
"left": [],
"center": [],
"right": [
{
"VtFooterBottom": {
"config": {
"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": {
"config": {
@ -277,10 +315,26 @@
"buttonClassName": "bg-[#C4622C] w-[90px]",
"socialsClassName": "mt-4 gap-8",
"socials": [
{ "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" },
{ "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"
},
{
"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": {
"config": {
"variant": "default",
"className": "bg-custom-gradient",
"ImageDisplayer": {
"config": {
"duration": 20,
"images": ["./banner-hero.webp", "./banner-hero.webp"],
"links": []
}
}
"className": "bg-custom-gradient"
}
}
},

View File

@ -179,29 +179,33 @@
{
"Hero": {
"config": {
"variant": "default",
"className": "h-[35rem]",
"ImageDisplayer": {
"config": {
"duration": 1,
"duration": 0,
"images": [
"./banner-hero.webp",
"./banner-hero.webp"
"./banner-hero-1.webp",
"./banner-hero-2.webp"
],
"links": []
"links": ["/", "/account", "/product"]
}
},
"left":[{
"left": [
{
"VtCtaBanner": {
"config": {
"className": "ml-[120px]",
"variant": "default",
"className": "left-[120px] top-[80px]",
"titleTextClassName": "leading-normal",
"tagText": "Besonders Aktion",
"titleText": "Willkommen in Vibentec IT Shop",
"descriptionText": "Insert the accordion description here. It would look better as two lines of text or more.",
"buttonText": "Zum Einkaufen"
}
}
}],
}
],
"center": [],
"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) {
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} />
</div>
)
}
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>
<Heading
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,
} from "vibentec/component-map"
import { clx } from "@medusajs/ui"
import BannerHeroVibentec from "../hero/hero-vibentec"
import BannerHeroDefault from "../hero/hero-default"
import BannerHero from "./banner-hero"
import { DynamicLayoutRenderer } from "vibentec/renderer"
interface BannerProps {
variant: "vibentec" | "default"
className?: string
}
export default async function Hero({
nodes,
context,
@ -18,7 +13,7 @@ export default async function Hero({
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = (nodes.config as BannerProps) ?? {}
const props = nodes.config ?? {}
const left = nodes.config?.left ?? []
const center = nodes.config?.center ?? []
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",
props.className
)
if (!props.variant) return null
const variants = {
vibentec: BannerHeroVibentec,
default: BannerHeroDefault,
}
const Component = variants[props.variant]
return (
<div className={heroClassName}>
<Component node={nodes} context={context} />
<div className="absolute inset-0 z-20 w-full h-full">
<nav className="content-container txt-xsmall-plus flex items-center justify-between w-full h-full text-small-regular">
<div className="flex items-center gap-x-4 w-full h-full">
<BannerHero node={nodes} context={context} />
<div className="absolute z-20">
<nav className="content-container txt-xsmall-plus flex items-center justify-between text-small-regular">
<div className="flex items-center gap-x-4">
{left && <DynamicLayoutRenderer nodes={left} context={context} />}
</div>
<div className="flex items-center gap-x-4 w-full h-full">
<div className="flex items-center gap-x-4">
{center && (
<DynamicLayoutRenderer nodes={center} context={context} />
)}
</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} />}
</div>
</nav>

View File

@ -44,6 +44,7 @@
display: grid;
position: absolute;
top: 0;
z-index: 1000;
width: 100%;
height: 100%;
grid-template-columns: auto 1fr;
@ -57,6 +58,7 @@
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
}
.embla__button {
--text-high-contrast-rgb-value: 49, 49, 49;
@ -74,7 +76,7 @@
margin: 0;
width: 3.6rem;
height: 3.6rem;
z-index: 1;
z-index: 1001;
border-radius: 50%;
color: var(--text-body);
display: flex;
@ -98,6 +100,7 @@
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
z-index: 1000;
}
.embla__dot {
--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 { DotButton, useDotButton } from "./carousel-dot-button"
import { NextButton, PrevButton, usePrevNextButtons } from "./carousel-arrow-button"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
export function VtCarousel({
nodes,
context,
@ -18,13 +20,21 @@ export function VtCarousel({
}) {
const props = nodes.config ?? {}
const { options } = props as any
const images: string[] = (props as any).images ?? (props as any).slides ?? []
const links: (string | undefined)[] = (props as any).links ?? []
const images: string[] = props.images ?? props.slides ?? []
const links: (string | undefined)[] = props.links ?? []
const durationSeconds: number = props.duration ?? 4
const showControls = images.length > 1
const plugins = useMemo(() => [
Autoplay({ delay: Math.max(1, durationSeconds) * 1000, stopOnInteraction: false, stopOnMouseEnter: true, })
], [durationSeconds])
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 { selectedIndex, scrollSnaps, onDotButtonClick } =
@ -41,12 +51,12 @@ export function VtCarousel({
<div className={styles["embla__viewport"]} ref={emblaRef}>
<div className={styles["embla__container"]}>
{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"]}>
{links[index] ? (
<a href={links[index]}>
<LocalizedClientLink href={links[index]}>
<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"]} />
)}
@ -56,6 +66,7 @@ export function VtCarousel({
</div>
</div>
{showControls && (
<div className={styles["embla__controls"]}>
<div className={styles["embla__buttons"]}>
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
@ -75,6 +86,7 @@ export function VtCarousel({
))}
</div>
</div>
)}
</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"
import { Button, clx } from "@medusajs/ui"
import { ChevronRightMini } from "@medusajs/icons"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { DefaultCtaBanner } from "./default-cta"
import { BearCtaBanner } from "./bear-cta"
export function VtCtaBanner({
nodes,
@ -14,27 +14,14 @@ export function VtCtaBanner({
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="inline-flex items-center rounded-full bg-[#FCE9DA] text-[#E68445] px-3 py-1 text-sm font-medium">
{props.tagText}
</div>
const variant = props.variant ?? "default"
<h1 className="mt-4 text-[#11314E] font-semibold leading-tight text-[56px]">
{props.titleText}
</h1>
<p className="mt-5 text-[#285A86] text-[16px] sm:text-xl opacity-80">
{props.descriptionText}
</p>
<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>
)
const variants: Record<string, any> = {
default: DefaultCtaBanner,
"3bear": BearCtaBanner,
}
const Component = variants[variant] || DefaultCtaBanner
return <Component nodes={nodes} context={context} />
}

View File

@ -2,7 +2,7 @@ import fs from "fs"
import path from "path"
import { jsonFileNames } from "./devJsonFileNames";
const fileName = jsonFileNames.namVibentec;
const fileName = jsonFileNames.namStarter;
export async function loadDesignConfig() {
const filePath = path.join(process.cwd(), "config", fileName)