refactor: make dynamic config loader json file and create template for 3bear design
This commit is contained in:
parent
51d6ee2051
commit
1bace8a023
|
|
@ -0,0 +1,308 @@
|
|||
[
|
||||
{
|
||||
"AnnouncementBanner": {
|
||||
"props": {
|
||||
"label": [
|
||||
{
|
||||
"text": "Free shipping on orders over $100",
|
||||
"className": "font-medium"
|
||||
},
|
||||
{
|
||||
"text": ".",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"text": "Free gift with every purchase",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"text": ".",
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"text": "Free returns",
|
||||
"className": ""
|
||||
}
|
||||
],
|
||||
"className": "bg-[#009b93] text-white"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Nav": {
|
||||
"props": {},
|
||||
"children": [
|
||||
{
|
||||
"Div": {
|
||||
"props": {
|
||||
"className": "flex items-center h-full ml-16"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"Image": {
|
||||
"props": {
|
||||
"src": "https://3bears.de/cdn/shop/files/3Bears_Logo-Schutzzone_negativ_RGB.png?v=1676382997&width=335",
|
||||
"alt": "Medusa Store",
|
||||
"width": 160,
|
||||
"height": 40
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Div": {
|
||||
"props": {
|
||||
"className": "flex items-center h-full gap-10"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenus": {
|
||||
"props": {
|
||||
"label": "Shop",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-categories-link",
|
||||
"isShowArrow": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "All Categories",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-all-categories-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "New Arrivals",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-new-arrivals-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "Best Sellers",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-best-sellers-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenus": {
|
||||
"props": {
|
||||
"label": "About us",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-categories-link",
|
||||
"isShowArrow": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "All Categories",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-all-categories-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "New Arrivals",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-new-arrivals-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "Best Sellers",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-best-sellers-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenus": {
|
||||
"props": {
|
||||
"label": "About our product",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-categories-link",
|
||||
"isShowArrow": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "All Categories",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-all-categories-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "New Arrivals",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-new-arrivals-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "Best Sellers",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-best-sellers-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"LocalizedClientLink": {
|
||||
"props": {
|
||||
"href": "/recipe",
|
||||
"label": "Recipes",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-register-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"LocalizedClientLink": {
|
||||
"props": {
|
||||
"href": "/meet-harry-kane",
|
||||
"label": "Meet Harry Kane",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-register-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Div": {
|
||||
"props": {
|
||||
"className": "flex items-center gap-x-6 h-full justify-end"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenus": {
|
||||
"props": {
|
||||
"label": "Germany (EUR €)",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-categories-link",
|
||||
"isShowArrow": true
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "All Categories",
|
||||
"className": "hover:text-ui-fg-base",
|
||||
"data-testid": "nav-all-categories-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "New Arrivals",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-new-arrivals-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"DropdownMenuItems": {
|
||||
"props": {
|
||||
"label": "Best Sellers",
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-best-sellers-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"SearchButton": {
|
||||
"props": {
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-account-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"UserButton": {
|
||||
"props": {
|
||||
"className": "hover:text-ui-fg-base flex items-center gap-2",
|
||||
"data-testid": "nav-account-link"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Suspense": {
|
||||
"props": {
|
||||
"fallback": [
|
||||
{
|
||||
"LocalizedClientLink": {
|
||||
"props": {
|
||||
"href": "/cart",
|
||||
"label": "Cart (0)",
|
||||
"className": "hover:text-ui-fg-base flex gap-2",
|
||||
"data-testid": "nav-cart-link"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"CartButton": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"CartMismatchBanner": {
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"FreeShippingPriceNudge": {
|
||||
"variant": "popup"
|
||||
}
|
||||
},
|
||||
{
|
||||
"PropsChildren": {}
|
||||
},
|
||||
{
|
||||
"Footer": {
|
||||
"copyrightText": "© 2025 MyShop"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -4,15 +4,17 @@ import { listCartOptions, retrieveCart } from "@lib/data/cart"
|
|||
import { retrieveCustomer } from "@lib/data/customer"
|
||||
import { getBaseURL } from "@lib/util/env"
|
||||
import { StoreCartShippingOption } from "@medusajs/types"
|
||||
import { DynamicLayoutRenderer } from "../../../vibentec/renderer"
|
||||
import { LayoutContext, LayoutComponentNode, } from "../../../vibentec/component-map"
|
||||
import { LayoutContext, LayoutComponentNode } from "vibentec/component-map"
|
||||
import { DynamicLayoutRenderer } from "vibentec/renderer"
|
||||
import { loadDesignConfig } from "vibentec/configloader"
|
||||
import { DESIGN_JSON_FILE } from "./config-json-file"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(getBaseURL()),
|
||||
}
|
||||
|
||||
export default async function PageLayout(props: { children: React.ReactNode }) {
|
||||
// Choose which design JSON to load. Swap this constant as needed.
|
||||
const customer = await retrieveCustomer()
|
||||
const cart = await retrieveCart()
|
||||
let shippingOptions: StoreCartShippingOption[] = []
|
||||
|
|
@ -23,14 +25,16 @@ export default async function PageLayout(props: { children: React.ReactNode }) {
|
|||
shippingOptions = shipping_options
|
||||
}
|
||||
|
||||
const nodes: LayoutComponentNode[] = await loadDesignConfig()
|
||||
const nodes: LayoutComponentNode[] = await loadDesignConfig(
|
||||
DESIGN_JSON_FILE[1].file
|
||||
)
|
||||
const context: LayoutContext = {
|
||||
customer,
|
||||
cart,
|
||||
shippingOptions,
|
||||
contentChildren: props.children,
|
||||
designId: DESIGN_JSON_FILE[1].id,
|
||||
}
|
||||
|
||||
|
||||
return <DynamicLayoutRenderer nodes={nodes} context={context} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
PopoverPanel,
|
||||
Transition,
|
||||
} from "@headlessui/react"
|
||||
import { ShoppingBag } from "@medusajs/icons"
|
||||
import { convertToLocale } from "@lib/util/money"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button } from "@medusajs/ui"
|
||||
|
|
@ -82,10 +83,12 @@ const CartDropdown = ({
|
|||
<Popover className="relative h-full">
|
||||
<PopoverButton className="h-full">
|
||||
<LocalizedClientLink
|
||||
className="hover:text-ui-fg-base"
|
||||
className="hover:text-ui-fg-base mr-10 flex items-center"
|
||||
href="/cart"
|
||||
data-testid="nav-cart-link"
|
||||
>{`Cart (${totalItems})`}</LocalizedClientLink>
|
||||
>
|
||||
<ShoppingBag />
|
||||
</LocalizedClientLink>
|
||||
</PopoverButton>
|
||||
<Transition
|
||||
show={cartDropdownOpen}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { MagnifyingGlassMini } from "@medusajs/icons"
|
||||
|
||||
type SearchButtonProps = {
|
||||
label?: string
|
||||
shortcut?: string
|
||||
onClick?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function SearchButton({
|
||||
label = "Search",
|
||||
shortcut = "⌘K",
|
||||
onClick,
|
||||
className,
|
||||
}: SearchButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={
|
||||
[
|
||||
"w-[250px] h-11 justify-between gap-3 border border-ui-border-base bg-ui-bg-subtle hover:bg-ui-bg-field-hover rounded-lg",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<MagnifyingGlassMini className="text-ui-fg-base" />
|
||||
<span className="text-ui-fg-base">{label}</span>
|
||||
</span>
|
||||
<span className="flex items-center rounded-md border border-ui-border-base px-2 py-0.5 bg-white text-xs text-ui-fg-muted">
|
||||
{shortcut}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
|
||||
type TextItem = { text: string; className?: string }
|
||||
|
||||
export default async function AnnouncementBanner({
|
||||
className,
|
||||
label,
|
||||
...props
|
||||
}: { className: string; label: TextItem[] }) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="container mx-auto flex justify-center items-center py-2">
|
||||
<div className="flex items-center gap-20">
|
||||
|
||||
{label.map((item, index) => {
|
||||
return (
|
||||
<div key={`${index}-${item.text}`} className={`last:mr-0 ${item.className || ""}`}>
|
||||
{item.text}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use client'
|
||||
import { DropdownMenu } from "@medusajs/ui"
|
||||
import { ChevronDownMini } from "@medusajs/icons"
|
||||
export default function DropdownMenuComponent({
|
||||
children,
|
||||
props,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
props?: {
|
||||
className?: string
|
||||
"data-testid"?: string
|
||||
label?: string
|
||||
isShowArrow?: boolean
|
||||
}
|
||||
}) {
|
||||
const itemNodes = Array.isArray(children) ? children : []
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
className={props?.className}
|
||||
data-testid={props?.["data-testid"]}
|
||||
>
|
||||
{props?.label ?? "Menu"} {props?.isShowArrow ? <ChevronDownMini /> : null}
|
||||
</button>
|
||||
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
{itemNodes.map((item, index) => {
|
||||
const props = item?.DropdownMenuItems?.props
|
||||
return props ? <DropdownMenu.Item key={props.label ?? index} className={props.className} data-testid={props["data-testid"]}>{props.label}</DropdownMenu.Item> : null
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,19 +2,21 @@ import { listRegions } from "@lib/data/regions"
|
|||
import { StoreRegion } from "@medusajs/types"
|
||||
import SideMenu from "@modules/layout/components/side-menu"
|
||||
import { DynamicLayoutRenderer, DynamicLayoutRendererProps } from "vibentec/renderer"
|
||||
import { clx } from "@medusajs/ui"
|
||||
|
||||
export default async function VtNav({ nodes, context }: DynamicLayoutRendererProps) {
|
||||
export default async function VtNav({ nodes, context, className }: DynamicLayoutRendererProps & { className?: string }) {
|
||||
console.log({nodes, context})
|
||||
const regions = await listRegions().then((regions: StoreRegion[]) => regions)
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 inset-x-0 z-50 group">
|
||||
<header className="relative h-16 mx-auto border-b duration-200 bg-white border-ui-border-base">
|
||||
<nav className="content-container txt-xsmall-plus text-ui-fg-subtle flex items-center justify-between w-full h-full text-small-regular">
|
||||
<div className="flex-1 basis-0 h-full flex items-center">
|
||||
<header className={clx("relative mx-auto border-b duration-200 border-ui-border-base", className ?? "bg-white") }>
|
||||
<nav className="flex justify-between w-full items-center h-full">
|
||||
{/* <div className="flex-1 basis-0 h-full flex items-center">
|
||||
<div className="h-full">
|
||||
<SideMenu regions={regions} />
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{nodes && (
|
||||
<DynamicLayoutRenderer nodes={nodes} context={context} />
|
||||
|
|
|
|||
|
|
@ -2,10 +2,18 @@ import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner"
|
|||
import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge"
|
||||
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
||||
import CartButton from "@modules/layout/components/cart-button"
|
||||
|
||||
import { DynamicLayoutRenderer } from "./renderer"
|
||||
import React from "react"
|
||||
import VtNav from "@modules/layout/templates/vt-nav"
|
||||
import VtFooter from "@modules/layout/templates/vt-footer"
|
||||
import AnnouncementBannerDefault from "@modules/layout/templates/3bear-template/announcement-bar"
|
||||
import AnnouncementBannerDrsquatch from "@modules/layout/templates/drsquatch-template/announcement-bar"
|
||||
import { Button } from "@medusajs/ui"
|
||||
import { User, MagnifyingGlassMini, Heart } from "@medusajs/icons"
|
||||
import DropdownMenuComponent from "@modules/layout/templates/dropdown-menu/dropdown-menu"
|
||||
import AnnouncementBannerVibenTec from "@modules/layout/templates/vibentec-template/announcement-bar"
|
||||
import SearchButton from "@modules/layout/components/search-button"
|
||||
|
||||
export interface LayoutComponentDefinition {
|
||||
props?: Record<string, any>
|
||||
|
|
@ -15,43 +23,118 @@ export interface LayoutComponentDefinition {
|
|||
//maps key = componentName to value = props + children
|
||||
export type LayoutComponentNode = Record<string, LayoutComponentDefinition>
|
||||
|
||||
export interface LayoutContext {
|
||||
customer: any;
|
||||
cart: any;
|
||||
shippingOptions: any[];
|
||||
contentChildren: React.ReactNode;
|
||||
export interface LayoutContext {
|
||||
customer: any
|
||||
cart: any
|
||||
shippingOptions: any[]
|
||||
contentChildren: React.ReactNode
|
||||
designId?: string
|
||||
}
|
||||
|
||||
export type ComponentRenderer = {
|
||||
render: (entry: LayoutComponentDefinition, ctx: LayoutContext) => React.ReactNode
|
||||
render: (
|
||||
entry: LayoutComponentDefinition,
|
||||
ctx: LayoutContext
|
||||
) => React.ReactNode
|
||||
}
|
||||
|
||||
// Utility, wenn eine Komponente nur props hat und keine children
|
||||
const simple = (Component: React.ComponentType<any>): ComponentRenderer => ({
|
||||
render: (entry) => <Component {...entry.props} />
|
||||
render: (entry) => <Component {...entry.props} />,
|
||||
})
|
||||
|
||||
// Helper für Kinder-Rendering
|
||||
const renderChildren = (entry: LayoutComponentDefinition, ctx: LayoutContext) =>
|
||||
entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null
|
||||
entry.children ? (
|
||||
<DynamicLayoutRenderer nodes={entry.children} context={ctx} />
|
||||
) : null
|
||||
|
||||
// Map design identifiers to banner component variants (no conditionals)
|
||||
const AnnouncementBannerVariants: Record<string, React.ComponentType<any>> = {
|
||||
drsquatch: AnnouncementBannerDrsquatch,
|
||||
vibentec: AnnouncementBannerVibenTec,
|
||||
"3bear": AnnouncementBannerDefault,
|
||||
"medusa-starter": AnnouncementBannerDefault,
|
||||
default: AnnouncementBannerDefault,
|
||||
}
|
||||
|
||||
// Component Map
|
||||
export const componentMap: Record<string, ComponentRenderer> = {
|
||||
Nav: {
|
||||
render: (entry: any, ctx: LayoutContext) => ( <VtNav nodes={entry.children} context={ctx} /> ),
|
||||
AnnouncementBanner: {
|
||||
render: (entry: any, ctx: LayoutContext) => {
|
||||
const key = ctx.designId ?? "default"
|
||||
const Comp =
|
||||
AnnouncementBannerVariants[key] ?? AnnouncementBannerVariants.default
|
||||
return <Comp {...entry.props} nodes={entry.children} context={ctx} />
|
||||
},
|
||||
},
|
||||
Nav: {
|
||||
render: (entry: any, ctx: LayoutContext) => (
|
||||
<VtNav {...entry.props} nodes={entry.children} context={ctx} />
|
||||
),
|
||||
},
|
||||
Div: {
|
||||
render: (entry: any, ctx: LayoutContext) => (
|
||||
<div {...entry.props}>
|
||||
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
|
||||
{entry.children ? (
|
||||
<DynamicLayoutRenderer nodes={entry.children} context={ctx} />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
Text: {
|
||||
render: (entry: any, ctx: LayoutContext) => (
|
||||
<div {...entry.props}>{entry.props.label}</div>
|
||||
),
|
||||
},
|
||||
DropdownMenus: {
|
||||
render: (entry: any, ctx: LayoutContext) => (
|
||||
<DropdownMenuComponent {...entry} />
|
||||
),
|
||||
},
|
||||
LocalizedClientLink: {
|
||||
render: (entry: any) => (
|
||||
<LocalizedClientLink {...entry.props}>{entry.props.label}</LocalizedClientLink>
|
||||
)
|
||||
<LocalizedClientLink {...entry.props}>
|
||||
{entry.props.label}
|
||||
</LocalizedClientLink>
|
||||
),
|
||||
},
|
||||
Image: {
|
||||
render: (entry: any) => {
|
||||
console.log(entry.props)
|
||||
return <img {...entry.props} alt={entry.props?.alt ?? ""} />
|
||||
},
|
||||
},
|
||||
Button: {
|
||||
render: (entry: any, ctx: LayoutContext) => (
|
||||
<Button {...entry.props}>{entry.props.label}</Button>
|
||||
),
|
||||
},
|
||||
SearchButton: {
|
||||
render: (entry: any) => (
|
||||
<Button variant="transparent">
|
||||
<MagnifyingGlassMini {...entry.props}>
|
||||
{entry.props.label}
|
||||
</MagnifyingGlassMini>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
InputSearchButton: {
|
||||
render: (entry: any) => <SearchButton {...entry.props} />,
|
||||
},
|
||||
UserButton: {
|
||||
render: (entry: any) => (
|
||||
<Button variant="transparent">
|
||||
<User {...entry.props}>{entry.props.label}</User>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
FavoriteButton: {
|
||||
render: (entry: any) => (
|
||||
<Button variant="transparent">
|
||||
<Heart {...entry.props}>{entry.props.label}</Heart>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
CartButton: simple(CartButton),
|
||||
Suspense: {
|
||||
|
|
@ -63,14 +146,16 @@ export const componentMap: Record<string, ComponentRenderer> = {
|
|||
) : null
|
||||
}
|
||||
>
|
||||
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
|
||||
{entry.children ? (
|
||||
<DynamicLayoutRenderer nodes={entry.children} context={ctx} />
|
||||
) : null}
|
||||
</React.Suspense>
|
||||
),
|
||||
},
|
||||
CartMismatchBanner: simple(CartMismatchBanner),
|
||||
FreeShippingPriceNudge: simple(FreeShippingPriceNudge),
|
||||
PropsChildren: {
|
||||
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
|
||||
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
|
||||
},
|
||||
Footer: simple(VtFooter),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
const fileName = "ste.medusa-starter.design.json";
|
||||
|
||||
export async function loadDesignConfig() {
|
||||
const filePath = path.join(process.cwd(), "config", fileName)
|
||||
export async function loadDesignConfig(designFile: string) {
|
||||
const filePath = path.join(process.cwd(), "config", designFile)
|
||||
const fileData = await fs.promises.readFile(filePath, "utf-8")
|
||||
return JSON.parse(fileData)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
import React from "react"
|
||||
import { LayoutComponentNode, LayoutContext, componentMap } from "./component-map"
|
||||
import {
|
||||
LayoutComponentNode,
|
||||
LayoutContext,
|
||||
componentMap,
|
||||
} from "./component-map"
|
||||
|
||||
export interface DynamicLayoutRendererProps {
|
||||
nodes: LayoutComponentNode[]
|
||||
context: LayoutContext
|
||||
}
|
||||
|
||||
export function DynamicLayoutRenderer({ nodes, context } : DynamicLayoutRendererProps) {
|
||||
return nodes.map((entry, index) => {
|
||||
const [key, value] = Object.entries(entry)[0] as [string, any]
|
||||
const component = componentMap[key]
|
||||
if (!component) return null
|
||||
return <React.Fragment key={index}>{component.render(value, context)}</React.Fragment>
|
||||
})
|
||||
}
|
||||
export function DynamicLayoutRenderer({
|
||||
nodes,
|
||||
context,
|
||||
}: DynamicLayoutRendererProps) {
|
||||
return nodes.map((entry, index) => {
|
||||
const [key, value] = Object.entries(entry)[0] as [string, any]
|
||||
const component = componentMap[key]
|
||||
if (!component) return null
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{component.render(value, context)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
|
||||
export interface LayoutContext {
|
||||
customer: HttpTypes.StoreCustomer | null
|
||||
cart: HttpTypes.StoreCart | null
|
||||
shippingOptions: HttpTypes.StoreShippingOption[]
|
||||
contentChildren: React.ReactNode
|
||||
}
|
||||
|
||||
export interface LayoutComponentDefinition<P = Record<string, unknown>> {
|
||||
props?: P
|
||||
children?: LayoutComponentNode[]
|
||||
}
|
||||
|
||||
// Maps key = componentName to value = props + children
|
||||
export type LayoutComponentNode = Record<string, LayoutComponentDefinition>
|
||||
|
||||
export type ComponentRenderer<P = unknown> = {
|
||||
render: (entry: LayoutComponentDefinition<P>, ctx: LayoutContext) => React.ReactNode
|
||||
}
|
||||
|
||||
export type ComponentMap = Record<string, ComponentRenderer>
|
||||
|
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
"./src/components/**/*.{js,ts,jsx,tsx}",
|
||||
"./src/modules/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}",
|
||||
"./config/**/*.json",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue