feat: create components and map with data json file of 3bear design

This commit is contained in:
Nam Doan 2025-11-27 14:02:13 +07:00
parent c8853bac1c
commit b76719fb32
9 changed files with 307 additions and 20 deletions

View File

@ -55,6 +55,138 @@
"objectFit": "contain"
}
}
},
{
"VtMegaMenu": {
"config": {
"navLabel": {
"text": "Shop",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
}
}
}
},
{
"Dropdown": {
"config": {
"trigger": {
"text": "Über Uns",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
},
"items": [
{
"text": "Unser Unternehmen",
"href": "/"
},
{
"text": "Loren ipsum",
"href": "/"
},
{
"text": "Not a Link"
}
]
}
}
},
{
"Dropdown": {
"config": {
"trigger": {
"text": "Über unsere Produkte",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]",
"isShowArrow": true
},
"items": [
{
"text": "Unser Unternehmen",
"href": "/"
},
{
"text": "Loren ipsum",
"href": "/"
},
{
"text": "Not a Link"
}
]
}
}
},
{
"Link": {
"config": {
"label": "Rezepte",
"href": "/",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center mr-8 gap-1 hover:text-[#009b93]"
}
}
},
{
"Link": {
"config": {
"label": "Triff Harry Kane",
"href": "/",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center gap-1 hover:text-[#009b93]"
}
}
}
],
"right": [
{
"Dropdown": {
"config": {
"trigger": {
"icon": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/1200px-Flag_of_Germany.svg.png",
"text": "Germany (EUR)",
"className": "font-bold text-[1rem] text-[#003F31] flex items-center gap-1 hover:text-[#009b93]",
"isShowArrow": true
},
"items": [
{
"icon": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/1200px-Flag_of_Germany.svg.png",
"text": "Germany (EUR)",
"href": "/"
},
{
"icon": "https://upload.wikimedia.org/wikipedia/commons/2/20/Flag_of_the_Netherlands.svg",
"text": "Netherlands (EUR)",
"href": "/"
},
{
"icon": "https://upload.wikimedia.org/wikipedia/commons/8/88/Flag_of_Australia_%28converted%29.svg",
"text": "Australia (AUD)",
"href": "/"
}
]
}
}
},
{
"IconButton": {
"config": {
"variant": "search",
"className": "shadow-none"
}
}
},
{
"IconButton": {
"config": {
"variant": "user",
"className": "shadow-none"
}
}
},
{
"VtCartButton": {
"config": {
"variant": "button",
"className": "shadow-none"
}
}
}
]
}

View File

@ -1,13 +1,38 @@
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui"
import { Suspense } from "react";
import CartButton from "@modules/layout/components/cart-button";
import {
LayoutComponentDefinition,
LayoutContext,
} from "vibentec/component-map"
import { clx, IconButton } from "@medusajs/ui"
import { Suspense } from "react"
import CartButton from "@modules/layout/components/cart-button"
import { ShoppingBag } from "@medusajs/icons"
export const VtCartButton = ({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) => {
const CartIconButtonComponent = ({ className }: { className?: string }) => {
return (
<IconButton className={className}>
<ShoppingBag />
</IconButton>
)
}
export const VtCartButton = ({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) => {
const props = nodes.config ?? {}
const className = clx("hover:text-ui-fg-base flex gap-2", props.className)
const variants = {
link: <CartButton />,
button: <CartIconButtonComponent className={className} />,
}
if (!props.variant) return null
const fallBackComp = variants[props.variant as keyof typeof variants]
return (
<Suspense
fallback={
@ -20,7 +45,7 @@ export const VtCartButton = ({ nodes, context }: { nodes: LayoutComponentDefinit
</LocalizedClientLink>
}
>
<CartButton />
{fallBackComp}
</Suspense>
)
}

View File

@ -0,0 +1,70 @@
"use client"
import { DropdownMenu } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import ChevronDown from "@modules/common/icons/chevron-down"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default function VtDropdown({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
if (!props.trigger.text || props.items.length === 0) {
return null
}
return (
<DropdownMenu>
<DropdownMenu.Trigger
className={props.trigger.className + " flex items-center gap-1"}
>
{props.trigger.icon && (
<img
src={props.trigger.icon}
alt={props.trigger.text}
className="w-5 h-5 rounded-[50%] mr-3"
/>
)}
{props.trigger.text} {props.trigger.isShowArrow && <ChevronDown />}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{props.items.map(
(
item: {
text: string
className?: string
href?: string
icon?: string
},
index: number
) => (
<DropdownMenu.Item
key={item.text + index}
className={item.className || ""}
>
{item.icon && (
<img
src={item.icon}
alt={item.text}
className="w-5 h-5 rounded-[50%] mr-3"
/>
)}
{item.href ? (
<LocalizedClientLink href={item.href}>
{item.text}
</LocalizedClientLink>
) : (
item.text
)}
</DropdownMenu.Item>
)
)}
</DropdownMenu.Content>
</DropdownMenu>
)
}

View File

@ -1,19 +1,34 @@
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import React, { Suspense } from "react";
import SkeletonMegaMenu from "@modules/skeletons/components/vt-skeleton-mega-menu";
import MegaMenuWrapper from "@modules/layout/components/vt-mega-menu/mega-menu-wrapper";
import {
LayoutComponentDefinition,
LayoutContext,
} from "vibentec/component-map"
import React, { Suspense } from "react"
import SkeletonMegaMenu from "@modules/skeletons/components/vt-skeleton-mega-menu"
import MegaMenuWrapper from "@modules/layout/components/vt-mega-menu/mega-menu-wrapper"
export default function VtMegaMenu({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) {
interface MegaMenuProps {
navLabel: {
text: string
className?: string
}
}
export default function VtMegaMenu({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const { navLabel } = nodes.config as MegaMenuProps ?? {}
return (
<nav>
<ul className="space-x-4 hidden small:flex">
<li>
<Suspense fallback={<SkeletonMegaMenu />}>
<MegaMenuWrapper />
<MegaMenuWrapper navLabel={navLabel} />
</Suspense>
</li>
</ul>
</nav>
)
}

View File

@ -1,10 +1,12 @@
import { listCategories } from "@lib/data/categories"
import MegaMenu from "./mega-menu"
export async function MegaMenuWrapper() {
export async function MegaMenuWrapper({ navLabel }: { navLabel: { text: string; className?: string } }) {
const categories = await listCategories().catch(() => [])
return <MegaMenu categories={categories} />
return <MegaMenu navLabel={navLabel} categories={categories} />
}
export default MegaMenuWrapper

View File

@ -3,12 +3,16 @@
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import ChevronDown from "@modules/common/icons/chevron-down"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
const MegaMenu = ({
navLabel,
categories,
}: {
navLabel: { text: string; className?: string, isShowArrow?: boolean },
categories: HttpTypes.StoreProductCategory[]
}) => {
const [isHovered, setIsHovered] = useState(false)
@ -81,10 +85,13 @@ const MegaMenu = ({
className="z-50"
>
<LocalizedClientLink
className="hover:text-ui-fg-base hover:bg-neutral-100 rounded-full px-3 py-2"
className={clx(
"hover:text-ui-fg-base hover:bg-neutral-100 rounded-full px-3 py-2",
navLabel.className
)}
href="/store"
>
Products
{navLabel.text} {navLabel.isShowArrow && <ChevronDown />}
</LocalizedClientLink>
{isHovered && (
<div className="absolute top-full left-0 right-0 flex gap-32 py-10 px-20 bg-white border-b border-neutral-200 ">

View File

@ -0,0 +1,28 @@
import { IconButton } from "@medusajs/ui"
import { MagnifyingGlass, User, ShoppingBag } from "@medusajs/icons"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default function VtIconButton({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config || {}
const variantsIcon = {
search: MagnifyingGlass,
user: User,
cart: ShoppingBag
}
if (!props.variant) return null
const Icon = variantsIcon[props.variant as keyof typeof variantsIcon]
return (
<IconButton className={props?.className ?? ""}>
<Icon />
</IconButton>
)
}

View File

@ -51,7 +51,8 @@
@layer components {
.content-container {
@apply max-w-[1440px] w-full mx-auto px-6;
/* @apply max-w-[1440px] w-full mx-auto px-6; */
@apply w-full mx-auto px-6;
}
.contrast-btn {
@ -110,3 +111,6 @@
@apply text-[32px] leading-[44px] font-semibold;
}
}
[data-radix-popper-content-wrapper]{
z-index: 51 !important;
}

View File

@ -13,6 +13,8 @@ import VtMegaMenu from "@modules/layout/components/vt-mega-menu"
import VtLink from "@modules/layout/components/vt-linkbutton"
import VtSideMenu from "@modules/layout/components/vt-sidemenu"
import VtImage from "@modules/layout/templates/vt-image"
import VtDropdown from "@modules/layout/components/vt-dropdown"
import VtIconButton from "@modules/layout/templates/vt-icon-button"
type ComponentConfig = Record<string, any>;
@ -54,9 +56,11 @@ export const componentMap: Record<string, ComponentRenderer> = {
Banner: nodesContextRenderer(Banner),
HomeButton: nodesContextRenderer(HomeButton),
AccountButton: nodesContextRenderer(AccountButton),
IconButton: nodesContextRenderer(VtIconButton),
VtCartButton: nodesContextRenderer(VtCartButton),
Link: nodesContextRenderer(VtLink),
Image: nodesContextRenderer(VtImage),
Dropdown: nodesContextRenderer(VtDropdown),
CartMismatchBanner: configOnly(CartMismatchBanner),
FreeShippingPriceNudge: configOnly(FreeShippingPriceNudge),
PropsChildren: {