27 Commits

Author SHA1 Message Date
Nam Doan a2e5b56eb8 refactor: combine 3bear cta section into dynamic cta section 2025-12-19 10:10:57 +07:00
Nam Doan 7afffb3f99 feat: create design for 3bear and drsquatch hero banner, create variants cta section 2025-12-17 11:12:32 +07:00
Nam Doan 9e0f6b0071 feat: add medusa starter design hero banner
feat: add hero banner for vibentec design
feat: add CTA banner vibentec design and create carousel image display
2025-12-16 14:46:36 +07:00
yen.nguyen 07a52dca2b Merge pull request 'namds/refactor-base-layout' (#8) from namds/refactor-base-layout into main
Reviewed-on: #8
2025-12-15 07:44:38 +00:00
Nam Doan fbb13ae819 refactor: add some component in footer and implement function for component 2025-12-11 12:59:00 +07:00
Nam Doan 9097a6e566 refactor: remove singular component 2025-12-11 12:54:08 +07:00
Nam Doan fe881d5aed refactor: remove singular component and replace with wrapper component 2025-12-10 14:09:46 +07:00
Nam Doan baaa0e9c62 refactor: update 3bear design json file 2025-12-08 16:36:51 +07:00
Nam Doan 216a579564 feat: add component for footer 2025-12-08 16:36:16 +07:00
Nam Doan dbe12845b0 refactor: update config json file 2025-12-08 11:40:28 +07:00
Nam Doan f869d12c7a feat: reuse country select and refactor some component 2025-12-08 11:39:41 +07:00
Nam Doan d8e78b71e4 refactor: cart button with icon 2025-12-08 11:29:41 +07:00
Nam Doan 043cc4d11a update: create design footer for 3bear page 2025-12-03 22:43:34 +07:00
Nam Doan 1e99ead658 feat: add footer drSquatch page 2025-12-03 22:10:28 +07:00
Nam Doan f3fb70d073 refactor: update common input and text component 2025-12-03 22:10:07 +07:00
Nam Doan efe48a200f feat: create footer vibentec design and common component text 2025-12-02 10:59:28 +07:00
Nam Doan 154d52732d feat: implement footer and update json file design 2025-12-01 16:34:47 +07:00
Nam Doan c0b492b394 refactor: dynamic icon button with medusajs and customize icon 2025-12-01 16:34:06 +07:00
Nam Doan 2f9102c4c4 bugfix: add condition check undefined value mega menu 2025-11-28 13:40:02 +07:00
Nam Doan 0dfa281333 refactor: move component to correct folder 2025-11-28 13:34:39 +07:00
Nam Doan 06968a77f3 refactor: name folder of vtButton 2025-11-28 13:32:22 +07:00
Nam Doan 96387dac11 bugfix: style nav config style 2025-11-28 13:26:23 +07:00
Nam Doan 9be3fb44b4 bugfix: change variant cart button in drsquatch page 2025-11-28 13:15:36 +07:00
Nam Doan 497c060756 feat:create component add vibentec config design page 2025-11-28 13:12:00 +07:00
Nam Doan 09b01f1d6b feat: created Drsquatch header component and json file 2025-11-27 15:04:56 +07:00
Nam Doan b76719fb32 feat: create components and map with data json file of 3bear design 2025-11-27 14:02:13 +07:00
Nam Doan c8853bac1c feat: add header banner 3bear design template 2025-11-26 11:22:58 +07:00
24 changed files with 714 additions and 142 deletions
+60 -7
View File
@@ -177,6 +177,36 @@
]
}
},
{
"Hero": {
"config": {
"className": "h-[35rem]",
"ImageDisplayer": {
"config": {
"duration": 0,
"images": ["./banner-hero.webp"],
"links": ["/account"]
}
},
"left": [
{
"VtCtaBanner": {
"config": {
"className": "left-[120px] top-[80px] relative w-full p-12 flex flex-col items-start justify-center text-left bg-transperant border-none shadow-none",
"buttonTextClassName": "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",
"tagTextClassName": "text-[#0D382E] text-lg font-medium mb-2 bg-transparent",
"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 +243,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 +357,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"
]
}
}
}
+59 -5
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,14 +315,30 @@
"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"
}
]
}
}
}
}
]
}
}
+2 -1
View File
@@ -137,7 +137,8 @@
{
"Hero": {
"config": {
"variant": "default"
"variant": "default",
"className": "bg-custom-gradient"
}
}
},
+38 -5
View File
@@ -179,9 +179,35 @@
{
"Hero": {
"config": {
"variant": "vibentec",
"text": "alo",
"className": "bg-custom-gradient"
"className": "h-[35rem]",
"ImageDisplayer": {
"config": {
"duration": 5,
"images": [
"./banner-hero.webp",
"./banner-hero-1.webp",
"./banner-hero-2.webp"
],
"links": ["/", "/account", "/product"]
}
},
"left": [
{
"VtCtaBanner": {
"config": {
"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": []
}
}
},
@@ -218,7 +244,10 @@
"logoAlt": "Vibentec IT",
"title": "Der Wegbereiter für innovative IT-Lösungen",
"description": "Tauchen Sie ein in eine Welt modernster Technologien, zuverlässiger Support und proaktiver Innovation gemeinsam gestalten wir die digitale Zukunft Ihres Unternehmens.",
"cta": { "label": "Kontaktieren Sie uns", "href": "/" },
"cta": {
"label": "Kontaktieren Sie uns",
"href": "/"
},
"className": "",
"ctaClassName": "ml-8",
"titleClassName": "ml-8",
@@ -338,7 +367,11 @@
"VtFooterBottom": {
"config": {
"text": "©2025 Vibentec IT. All rights reserved",
"icons": ["Mastercard", "PayPal", "Visa"]
"icons": [
"Mastercard",
"PayPal",
"Visa"
]
}
}
}
+2
View File
@@ -21,6 +21,8 @@
"@radix-ui/react-accordion": "^1.2.1",
"@stripe/react-stripe-js": "^1.7.2",
"@stripe/stripe-js": "^1.29.0",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lodash": "^4.17.21",
"next": "^15.3.1",
"pg": "^8.11.3",
Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

+4 -1
View File
@@ -23,11 +23,14 @@ export default async function Home(props: {
const { collections } = await listCollections({
fields: "id, handle, title",
})
const res = await listCollections({
fields: "id, handle, title",
})
if (!collections || !region) {
return null
}
console.log(res, '--------------')
return (
<>
{/* <Hero /> */}
@@ -0,0 +1,45 @@
import { Github } from "@medusajs/icons"
import { Button, Heading } from "@medusajs/ui"
import { VtCarousel } from "../vt-carousel"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map"
export default function HeroDefault({ node, context }: { node: LayoutComponentDefinition; context: LayoutContext }) {
const props = node.config ?? {}
const imageDisplayer = props.ImageDisplayer
if (imageDisplayer) {
return (
<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-auto flex flex-col justify-center items-center text-center small:p-32 gap-6">
<span>
<Heading
level="h1"
className="text-3xl leading-10 text-ui-fg-base font-normal"
>
Ecommerce Starter Template
</Heading>
<Heading
level="h2"
className="text-3xl leading-10 text-ui-fg-subtle font-normal"
>
Powered by Medusa and Next.js
</Heading>
</span>
<a
href="https://github.com/medusajs/nextjs-starter-medusa"
target="_blank"
>
<Button variant="secondary">
View on GitHub
<Github />
</Button>
</a>
</div>
)
}
@@ -1,31 +0,0 @@
import { Github } from "@medusajs/icons"
import { Button, Heading } from "@medusajs/ui"
export default function HeroDefault() {
return (
<div className="absolute inset-0 z-10 flex flex-col justify-center items-center text-center small:p-32 gap-6">
<span>
<Heading
level="h1"
className="text-3xl leading-10 text-ui-fg-base font-normal"
>
Ecommerce Starter Template
</Heading>
<Heading
level="h2"
className="text-3xl leading-10 text-ui-fg-subtle font-normal"
>
Powered by Medusa and Next.js
</Heading>
</span>
<a
href="https://github.com/medusajs/nextjs-starter-medusa"
target="_blank"
>
<Button variant="secondary">
View on GitHub
<Github />
</Button>
</a>
</div>
)
}
@@ -1,48 +0,0 @@
import { Github } from "@medusajs/icons"
import { Button, clx, Heading } from "@medusajs/ui"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
interface Props {
node: LayoutComponentDefinition
context: LayoutContext
}
const HeroVibentec = ({ node, context }: Props) => {
const props = node.config ?? {}
return (
<div
className={clx(
"absolute inset-0 z-10 flex flex-col justify-center items-center text-center small:p-32 gap-6",
props.className
)}
>
<span>
<Heading
level="h1"
className="text-3xl leading-10 text-ui-fg-base font-normal"
>
{props.text ?? "Ecommerce Starter Template Vibentec"}
</Heading>
<Heading
level="h2"
className="text-3xl leading-10 text-ui-fg-subtle font-normal"
>
Powered by Medusa and Next.js
</Heading>
</span>
<a
href="https://github.com/medusajs/nextjs-starter-medusa"
target="_blank"
>
<Button variant="secondary">
View on GitHub
<Github />
</Button>
</a>
</div>
)
}
export default HeroVibentec
+23 -18
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
speed?: number
}
export default async function Hero({
nodes,
context,
@@ -18,23 +13,33 @@ 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 ?? []
const heroClassName = clx(
"h-[75vh] w-full border-b border-ui-border-base relative bg-ui-bg-subtle",
"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} />
<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">
{center && (
<DynamicLayoutRenderer nodes={center} context={context} />
)}
</div>
<div className="flex items-center gap-x-4 justify-end">
{right && <DynamicLayoutRenderer nodes={right} context={context} />}
</div>
</nav>
</div>
</div>
)
}
@@ -0,0 +1,75 @@
import React, { useCallback, useEffect, useState } from 'react'
import style from './index.module.css'
export const usePrevNextButtons = (emblaApi: any) => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollPrev()
}, [emblaApi])
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollNext()
}, [emblaApi])
const onSelect = useCallback((emblaApi: any) => {
setPrevBtnDisabled(!emblaApi.canScrollPrev())
setNextBtnDisabled(!emblaApi.canScrollNext())
}, [])
useEffect(() => {
if (!emblaApi) return
onSelect(emblaApi)
emblaApi.on('reInit', onSelect).on('select', onSelect)
}, [emblaApi, onSelect])
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick
}
}
export const PrevButton = (props: any) => {
const { children, ...restProps } = props
return (
<button
className={style['embla__button']}
type="button"
{...restProps}
>
<svg className={style['embla__button__svg']} viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
/>
</svg>
{children}
</button>
)
}
export const NextButton = (props: any) => {
const { children, ...restProps } = props
return (
<button
className={style['embla__button']}
type="button"
{...restProps}
>
<svg className={style['embla__button__svg']} viewBox="0 0 532 532">
<path
fill="currentColor"
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
/>
</svg>
{children}
</button>
)
}
@@ -0,0 +1,46 @@
import React, { useCallback, useEffect, useState } from 'react'
export const useDotButton = (emblaApi: any) => {
const [selectedIndex, setSelectedIndex] = useState(0)
const [scrollSnaps, setScrollSnaps] = useState([])
const onDotButtonClick = useCallback(
(index: number) => {
if (!emblaApi) return
emblaApi.scrollTo(index)
},
[emblaApi]
)
const onInit = useCallback((emblaApi: any) => {
setScrollSnaps(emblaApi.scrollSnapList())
}, [])
const onSelect = useCallback((emblaApi: any) => {
setSelectedIndex(emblaApi.selectedScrollSnap())
}, [])
useEffect(() => {
if (!emblaApi) return
onInit(emblaApi)
onSelect(emblaApi)
emblaApi.on('reInit', onInit).on('reInit', onSelect).on('select', onSelect)
}, [emblaApi, onInit, onSelect])
return {
selectedIndex,
scrollSnaps,
onDotButtonClick
}
}
export const DotButton = (props: any) => {
const { children, ...restProps } = props
return (
<button type="button" {...restProps}>
{children}
</button>
)
}
@@ -0,0 +1,139 @@
.embla {
width: 100%;
height: 100%;
position: relative;
margin: auto;
--slide-height: 19rem;
--slide-spacing: 1rem;
--slide-size: 100%;
}
.embla__viewport {
height: 100%;
overflow: hidden;
}
.embla__container {
display: flex;
height: 100%;
touch-action: pan-y pinch-zoom;
margin-left: calc(var(--slide-spacing) * -1);
--slide-spacing: 1rem;
}
.embla__slide {
--slide-size: 100%;
--slide-spacing: 1rem;
transform: translate3d(0, 0, 0);
flex: 0 0 var(--slide-size);
min-width: 0;
padding-left: var(--slide-spacing);
}
.embla__slide__number {
height: 100%;
font-size: 4rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.embla__slide__image {
width: 100%;
height: 100%;
object-fit: cover;
}
.embla__controls {
display: grid;
position: absolute;
top: 0;
z-index: 1000;
width: 100%;
height: 100%;
grid-template-columns: auto 1fr;
justify-content: space-between;
gap: 1.2rem;
}
.embla__buttons {
position: absolute;
top: 45%;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
}
.embla__button {
--text-high-contrast-rgb-value: 49, 49, 49;
--detail-high-contrast: rgb(192, 192, 192);
--text-body: rgb(54, 49, 61);
-webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
-webkit-appearance: none;
appearance: none;
background-color: transparent;
touch-action: manipulation;
text-decoration: none;
cursor: pointer;
border: 0;
padding: 0;
margin: 0;
width: 3.6rem;
height: 3.6rem;
z-index: 1001;
border-radius: 50%;
color: var(--text-body);
display: flex;
align-items: center;
justify-content: center;
}
.embla__button:disabled {
--detail-high-contrast: rgb(192, 192, 192);
color: var(--detail-high-contrast);
}
.embla__button__svg {
width: 35%;
height: 35%;
}
.embla__dots {
display: flex;
position: absolute;
bottom: 0;
left: 48%;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
z-index: 1000;
}
.embla__dot {
--text-high-contrast-rgb-value: 49, 49, 49;
--text-body: rgb(54, 49, 61);
-webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
-webkit-appearance: none;
appearance: none;
background-color: transparent;
touch-action: manipulation;
text-decoration: none;
cursor: pointer;
border: 0;
padding: 0;
margin: 0;
width: 1.6rem;
height: 1.6rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.embla__dot:after {
--detail-medium-contrast: rgb(234, 234, 234);
--detail-medium-contrast-rgb-value: 234, 234, 234;
box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast);
width: 0.42rem;
height: 0.42rem;
border-radius: 50%;
display: flex;
align-items: center;
content: "";
}
.embla__dot--selected:after {
--text-body: black;
box-shadow: inset 0 0 0 0.2rem var(--text-body);
}
@@ -0,0 +1,92 @@
"use client"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import styles from "./index.module.css"
import useEmblaCarousel from "embla-carousel-react"
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,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const { options } = props as any
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(() => {
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 } =
useDotButton(emblaApi)
const {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick,
} = usePrevNextButtons(emblaApi)
return (
<section className={styles["embla"]}>
<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 + src}>
<div className={styles["embla__slide__number"]}>
{links[index] ? (
<LocalizedClientLink href={links[index]}>
<img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} />
</LocalizedClientLink>
) : (
<img src={src} alt={`slide-${index + 1}`} className={styles["embla__slide__image"]} />
)}
</div>
</div>
))}
</div>
</div>
{showControls && (
<div className={styles["embla__controls"]}>
<div className={styles["embla__buttons"]}>
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
</div>
<div className={styles["embla__dots"]}>
{scrollSnaps.map((_, index) => (
<DotButton
key={index}
onClick={() => onDotButtonClick(index)}
className={[
styles["embla__dot"],
index === selectedIndex ? styles["embla__dot--selected"] : "",
].filter(Boolean).join(" ")}
/>
))}
</div>
</div>
)}
</section>
)
}
@@ -23,7 +23,6 @@ export default function VtCountrySelectClient({
const triggerText = props?.trigger?.text
const [items, setItems] = useState<{ text: string; label?: string }[]>([])
const { countryCode } = useParams()
console.log(regions)
useEffect(() => {
if (!regions || regions.length === 0) {
setItems([])
@@ -39,7 +38,6 @@ export default function VtCountrySelectClient({
.flat()
.filter((o) => o.text)
.sort((a, b) => (a.label ?? "").localeCompare(b.label ?? ""))
console.log(opts)
setItems(opts)
}, [regions])
@@ -0,0 +1,68 @@
"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
)}
>
{props.tagText && (
<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>
)}
{props.titleText && (
<h1
className={clx(
"mt-4 text-[#11314E] font-semibold leading-normal text-[56px]",
props.titleTextClassName
)}
>
{props.titleText}
</h1>
)}
{props.descriptionText && (
<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>
)
}
@@ -0,0 +1,25 @@
"use client"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { DefaultCtaBanner } from "./default-cta"
export function VtCtaBanner({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const variant = props.variant ?? "default"
const variants: Record<string, any> = {
default: DefaultCtaBanner,
}
const Component = variants[variant] || DefaultCtaBanner
return <Component nodes={nodes} context={context} />
}
+1 -1
View File
@@ -113,4 +113,4 @@
}
[data-radix-popper-content-wrapper]{
z-index: 51 !important;
}
}
+34 -19
View File
@@ -23,41 +23,53 @@ import VtFooterBottom from "@modules/layout/templates/vt-footer/vt-footer-bottom
import VtLogo from "@modules/layout/templates/vt-logo"
import VtFooterSignUp from "@modules/layout/templates/vt-footer/vt-footer-signup"
import Hero from "@modules/layout/templates/hero"
import { VtCarousel } from "@modules/layout/templates/vt-carousel"
import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner"
type ComponentConfig = Record<string, any>;
type ComponentConfig = Record<string, any>
export interface LayoutComponentDefinition {
config?: ComponentConfig
children?: LayoutComponentNode[]
}
export interface LayoutContext {
customer: any;
cart: any;
shippingOptions: any[];
contentChildren: React.ReactNode;
export interface LayoutContext {
customer: any
cart: any
shippingOptions: any[]
contentChildren: React.ReactNode
}
export type ComponentRenderer = {
render: (entry: LayoutComponentDefinition, ctx: LayoutContext) => React.ReactNode
render: (
entry: LayoutComponentDefinition,
ctx: LayoutContext
) => React.ReactNode
}
// Utility methods
const configOnly = (Component: React.ComponentType<any>): ComponentRenderer => ({
render: (entry) => <Component {...entry.config} />
const configOnly = (
Component: React.ComponentType<any>
): ComponentRenderer => ({
render: (entry) => <Component {...entry.config} />,
})
const nodesContextRenderer = (Component: React.ComponentType<any>): ComponentRenderer => ({
render: (entry: any, ctx: LayoutContext) => <Component nodes={entry} context={ctx} />
});
const nodesContextRenderer = (
Component: React.ComponentType<any>
): ComponentRenderer => ({
render: (entry: any, ctx: LayoutContext) => (
<Component nodes={entry} context={ctx} />
),
})
const renderChildren = (entry: LayoutComponentDefinition, ctx: LayoutContext) =>
entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null
entry.children ? (
<DynamicLayoutRenderer nodes={entry.children} context={ctx} />
) : null
// Component Map
export const componentMap: Record<string, ComponentRenderer> = {
Header: nodesContextRenderer(VtHeader),
Header: nodesContextRenderer(VtHeader),
Nav: nodesContextRenderer(VtNav),
Hero: nodesContextRenderer(Hero),
VtMegaMenu: nodesContextRenderer(VtMegaMenu),
@@ -77,17 +89,20 @@ export const componentMap: Record<string, ComponentRenderer> = {
CartMismatchBanner: configOnly(CartMismatchBanner),
FreeShippingPriceNudge: configOnly(FreeShippingPriceNudge),
PropsChildren: {
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
},
VtCtaBanner: nodesContextRenderer(VtCtaBanner),
VtFooterHero: nodesContextRenderer(VtFooterHero),
VtFooterBottom: nodesContextRenderer(VtFooterBottom),
VtFooterSignUp: nodesContextRenderer(VtFooterSignUp),
Footer: nodesContextRenderer(VtFooter)
Footer: nodesContextRenderer(VtFooter),
ImageDisplayer: nodesContextRenderer(VtCarousel),
}
export type ComponentName = keyof typeof componentMap
// //maps key = componentName to value = props + children
// export type LayoutComponentNode = Record<string, LayoutComponentDefinition>
export type LayoutComponentNode = { [K in ComponentName]: LayoutComponentDefinition }[ComponentName]
export type LayoutComponentNode = {
[K in ComponentName]: LayoutComponentDefinition
}[ComponentName]
+1 -1
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)
-3
View File
@@ -13,9 +13,6 @@ module.exports = {
],
theme: {
extend: {
backgroundImage: {
'custom-gradient': 'linear-gradient(90deg, #E6EFFC 0%, #285A86 100%)',
},
transitionProperty: {
width: "width margin",
height: "height",