refactor: make dynamic config loader json file and create template for 3bear design

This commit is contained in:
Nam Doan
2025-11-24 15:42:38 +07:00
parent 51d6ee2051
commit 1bace8a023
12 changed files with 580 additions and 41 deletions
+102 -17
View File
@@ -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),
}
}
+2 -4
View File
@@ -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)
}
+20 -9
View File
@@ -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>
)
})
}
+23
View File
@@ -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>