added packages to support markdown

added dummy pages for about and contact.
updated tailwind.config.js to include config/*.json to scan for classes.
refactored system basics. Removed component-props, using generic LayoutComponentDefinition and LayoutContext. cleanup component-map. Updated renderer to be more failsafe.

new abstracted components:
 vt-mega-menu,
vt-sidemenu,
vt-banner with 3 variants,
vt-header,
vt-homebutton,
vt-cartbutton,
vt-accountbutton,
vt-linkbutton,
This commit is contained in:
Stephan Stang 2025-11-25 15:17:18 +01:00
parent 51d6ee2051
commit 39fe0c81e5
30 changed files with 2849 additions and 142 deletions

View File

@ -1,68 +1,35 @@
[
{
"Nav": {
"props": {},
"children": [
{
"Header" : {
"config" : {"sticky": true},
"children" : [
{
"Div": {
"props": { "className": "flex items-center h-full" },
"children": [
{
"LocalizedClientLink": {
"props": {
"href": "/",
"label": "Medusa Store",
"className": "bg-black txt-compact-xlarge-plus hover:text-ui-fg-base uppercase",
"data-testid": "nav-store-link"
"Nav": {
"config": {
"left": [
{ "VtSideMenu": {} }
],
"center": [
{ "HomeButton": { "config" : {"label":"Medusa Store"}} }
],
"right": [
{ "AccountButton": {
"config": {
"label": "Account",
"className": "hover:text-ui-fg-base"
}
}
}
}
]
}
},
{
"Div": {
"props": { "className": "flex items-center gap-x-6 h-full flex-1 basis-0 justify-end" },
"children": [
{
"LocalizedClientLink": {
"props": {
"href": "/account",
"label": "Account",
"className": "hover:text-ui-fg-base bg-black",
"data-testid": "nav-account-link"
}
}
},
{
"Suspense": {
"props": {
"fallback": [
{
"LocalizedClientLink": {
"props": {
"href": "/cart",
"label": "Cart (0)",
"className": "bg-black hover:text-ui-fg-base flex gap-2",
"data-testid": "nav-cart-link"
}
}
}
]
},
"children": [
{ "CartButton": {} }
]
}
}
]
},
{ "VtCartButton" : {} }
]
}
}
}
]
}
},
{ "CartMismatchBanner": { "show": true } },
{ "FreeShippingPriceNudge": { "variant": "popup" } },
{ "CartMismatchBanner": { "config": {"show": true} } },
{ "FreeShippingPriceNudge": { "config": {"variant": "popup" }} },
{ "PropsChildren" : {}},
{ "Footer": { "copyrightText": "© 2025 MyShop" } }
{ "Footer": { "config" : {"copyrightText": "© 2025 MyShop"} } }
]

View File

@ -0,0 +1,70 @@
[
{
"Header" : {
"config" : {"sticky": true},
"children" : [
{
"Banner": {
"config": {
"variant": "nav",
"className": "h-12 bg-[#E6EFFC] text-[#285A86]",
"left":[ { "Link": { "config" : {"label":"About us", "href":"/vt-about"}} }],
"center":[ { "Link": { "config" : {"label":"Contact", "href":"/vt-contact"}} }],
"right":[ { "HomeButton": { "config" : {"label":"Vibentec IT"}} }]
}
}
},
{
"Banner": {
"config": {
"variant": "ticker",
"speed":24,
"className": "h-12 bg-[#E6EFFC] text-[#285A86]",
"items":[ {
"Link": { "config" : {"label":"Vibentec IT"}}
}]
}
}
},
{
"Banner": {
"config": {
"variant": "cta",
"text": "**Black Friday Vorverkauf**\nKOSTENLOSES MISTERY GESCHENK ab 60€ <u>jetzt shoppen</u>",
"href" : "/",
"className": "h-12 bg-[#E6EFFC] text-[#285A86]"
}
}
},
{
"Nav": {
"config": {
"left": [
{ "VtMegaMenu": {} } ,
{ "HomeButton": { "config" : {"label":"Vibentec IT"}} }
],
"center": [
{ "AccountButton": {
"config": {
"label": "Accounto",
"bgColor": "#123456",
"textColor": "#abcdef",
"className": "hover:text-ui-fg-base"
}
}
}
],
"right": [
{ "VtCartButton" : {} }
]
}
}
}
]
}
},
{ "CartMismatchBanner": { "config": {"show": true} } },
{ "FreeShippingPriceNudge": { "config": {"variant": "popup" }} },
{ "PropsChildren" : {}},
{ "Footer": { "config" : {"copyrightText": "© 2025 MyShop"} } }
]

1416
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,9 @@
"react": "19.0.0-rc-66855b96-20241106",
"react-country-flag": "^3.1.0",
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
"server-only": "^0.0.1",
"tailwindcss-radix": "^2.8.0",
"webpack": "^5"

View File

@ -0,0 +1,12 @@
import React from "react";
export default function AboutPage() {
return (
<div className="content-container py-16">
<h1 className="text-3xl font-bold mb-4">Über uns</h1>
<p>
Willkommen bei Vibentec IT! Wir bieten maßgeschneiderte Softwarelösungen.
</p>
</div>
);
}

View File

@ -0,0 +1,12 @@
import React from "react";
export default function ContactPage() {
return (
<div className="content-container py-16">
<h1 className="text-3xl font-bold mb-4">Kontakt</h1>
<p>
Schreibe uns: <a href="mailto:info@vibentec-it.de">info@vibentec-it.de</a>
</p>
</div>
);
}

View File

@ -0,0 +1,27 @@
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui"
export const AccountButton = ({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) => {
const props = nodes.config ?? {}
const className = clx("hover:text-ui-fg-base", props.className);
const style: React.CSSProperties = {};
if (props.bgColor) style.backgroundColor = props.bgColor;
if (props.textColor) style.color = props.textColor;
const href = props.href ?? "/account"
const label = props.label ?? "Account"
return (
<div className="flex items-center h-full" style={style}>
<LocalizedClientLink
href={href}
className={className}
data-testid="nav-account-link"
>
{label}
</LocalizedClientLink>
</div>
)
}
export default AccountButton

View File

@ -0,0 +1,28 @@
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";
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)
return (
<Suspense
fallback={
<LocalizedClientLink
className={className}
href="/cart"
data-testid="nav-cart-link"
>
Cart (0)
</LocalizedClientLink>
}
>
<CartButton />
</Suspense>
)
}
export default VtCartButton

View File

@ -0,0 +1,24 @@
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui"
export const HomeButton = ({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) => {
const props = nodes.config ?? {}
const className = clx("txt-compact-xlarge-plus hover:text-ui-fg-base uppercase", props.className)
const href = props.href ?? "/"
const label = props.label ?? "Medusa Store"
return (
<div className="flex items-center h-full">
<LocalizedClientLink
href={href}
className={className}
data-testid="nav-store-link"
>
{label}
</LocalizedClientLink>
</div>
)
}
export default HomeButton

View File

@ -0,0 +1,23 @@
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui"
export const VtLink = ({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) => {
const props = nodes.config ?? {}
const className = clx("txt-compact-xlarge-plus hover:text-ui-fg-base", props.className)
const href = props.href ?? "/"
const label = props.label ?? "Medusa Store"
return (
<div className="flex items-center h-full">
<LocalizedClientLink
href={href}
className={className}
>
{label}
</LocalizedClientLink>
</div>
)
}
export default VtLink

View File

@ -0,0 +1,19 @@
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 }) {
return (
<nav>
<ul className="space-x-4 hidden small:flex">
<li>
<Suspense fallback={<SkeletonMegaMenu />}>
<MegaMenuWrapper />
</Suspense>
</li>
</ul>
</nav>
)
}

View File

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

View File

@ -0,0 +1,142 @@
"use client"
import { HttpTypes } from "@medusajs/types"
import { clx } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
const MegaMenu = ({
categories,
}: {
categories: HttpTypes.StoreProductCategory[]
}) => {
const [isHovered, setIsHovered] = useState(false)
const [selectedCategory, setSelectedCategory] = useState<
HttpTypes.StoreProductCategory["id"] | null
>(null)
const pathname = usePathname()
const mainCategories = categories.filter(
(category) => !category.parent_category_id
)
const getSubCategories = (categoryId: string) => {
return categories.filter(
(category) => category.parent_category_id === categoryId
)
}
let menuTimeout: NodeJS.Timeout | null = null
const handleMenuHover = () => {
if (menuTimeout) {
clearTimeout(menuTimeout)
}
setIsHovered(true)
}
const handleMenuLeave = () => {
menuTimeout = setTimeout(() => {
setIsHovered(false)
}, 300)
return () => {
if (menuTimeout) {
clearTimeout(menuTimeout)
}
}
}
let categoryTimeout: NodeJS.Timeout | null = null
const handleCategoryHover = (categoryId: string) => {
categoryTimeout = setTimeout(() => {
setSelectedCategory(categoryId)
}, 200)
return () => {
if (categoryTimeout) {
clearTimeout(categoryTimeout)
}
}
}
const handleCategoryLeave = () => {
if (categoryTimeout) {
clearTimeout(categoryTimeout)
}
}
useEffect(() => {
setIsHovered(false)
}, [pathname])
return (
<>
<div
onMouseEnter={handleMenuHover}
onMouseLeave={handleMenuLeave}
className="z-50"
>
<LocalizedClientLink
className="hover:text-ui-fg-base hover:bg-neutral-100 rounded-full px-3 py-2"
href="/store"
>
Products
</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 ">
<div className="flex flex-col gap-2">
{mainCategories.map((category) => (
<LocalizedClientLink
key={category.id}
href={`/categories/${category.handle}`}
className={clx(
"hover:bg-neutral-100 hover:cursor-pointer rounded-full px-3 py-2 w-fit font-medium",
selectedCategory === category.id && "bg-neutral-100"
)}
onMouseEnter={() => handleCategoryHover(category.id)}
onMouseLeave={handleCategoryLeave}
>
{category.name}
</LocalizedClientLink>
))}
</div>
{selectedCategory && (
<div className="grid grid-cols-4 gap-16">
{getSubCategories(selectedCategory).map((category) => (
<div key={category.id} className="flex flex-col gap-2">
<LocalizedClientLink
className="font-medium text-zinc-500 hover:underline"
href={`/categories/${category.handle}`}
>
{category.name}
</LocalizedClientLink>
<div className="flex flex-col gap-2">
{getSubCategories(category.id).map((subCategory) => (
<LocalizedClientLink
key={subCategory.id}
className="hover:underline"
href={`/categories/${subCategory.handle}`}
>
{subCategory.name}
</LocalizedClientLink>
))}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{isHovered && (
<div className="fixed inset-0 mt-[60px] blur-sm backdrop-blur-sm z-[-1]" />
)}
</>
)
}
export default MegaMenu

View File

@ -0,0 +1,20 @@
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import React from "react";
import SideMenu from "../side-menu";
import { listRegions } from "@lib/data/regions"
import { StoreRegion } from "@medusajs/types"
export default async function VtSideMenu({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) {
const regions = await listRegions().then((regions: StoreRegion[]) => regions)
return (
<div className="flex-1 basis-0 h-full flex items-center">
<div className="h-full">
<SideMenu regions={regions} />
</div>
</div>
)
}

View File

@ -0,0 +1,52 @@
"use client"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map"
import { clx } from "@medusajs/ui"
import ReactMarkdown from "react-markdown"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import rehypeRaw from "rehype-raw"
import remarkBreaks from "remark-breaks"
interface BannerCTAProps {
node: LayoutComponentDefinition
context: LayoutContext
}
export default function BannerCTA({ node, context }: BannerCTAProps) {
const props = node.config ?? {}
const text = props.text ?? ""
const href = props.href ?? undefined
const iconLeft = props.iconLeft
const iconRight = props.iconRight
const className = clx(
"content-container flex justify-center items-center text-center w-full h-full text-xs font-medium",
props.className
)
const content = (
<div className="flex items-center gap-2">
{iconLeft && <DynamicIcon name={iconLeft} />}
<div className="leading-none">
<ReactMarkdown
children={text}
remarkPlugins={[remarkBreaks]}
rehypePlugins={[rehypeRaw]}
/>
</div>
{iconRight && <DynamicIcon name={iconRight} />}
</div>
)
return href ? (
<LocalizedClientLink href={href} className={className}>
{content}
</LocalizedClientLink>
) : (
<div className={className}>{content}</div>
)
}
function DynamicIcon({ name }: { name: string }) {
return <span className="material-symbols-outlined text-sm">{name}</span>
}

View File

@ -0,0 +1,20 @@
import { DynamicLayoutRenderer } from "vibentec/renderer"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
export default function BannerNav({ node: node, context }: { node: LayoutComponentDefinition; context: LayoutContext }) {
const props = node.config ?? {};
return (
<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 items-center gap-x-4">
{props.left && <DynamicLayoutRenderer nodes={props.left} context={context} />}
</div>
<div className="flex items-center gap-x-4">
{props.center && <DynamicLayoutRenderer nodes={props.center} context={context} />}
</div>
<div className="flex items-center gap-x-4">
{props.right && <DynamicLayoutRenderer nodes={props.right} context={context} />}
</div>
</nav>
)
}

View File

@ -0,0 +1,12 @@
@keyframes bannerTicker {
0% { transform: translateX(100%); }
100% { transform: translateX(-100%); }
}
.ticker {
display: flex;
white-space: nowrap;
align-items: center;
animation: bannerTicker linear infinite;
height: 100%;
}

View File

@ -0,0 +1,15 @@
import styles from "./banner-ticker.module.css"
import { DynamicLayoutRenderer } from "vibentec/renderer"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map"
export default function BannerTicker({ node, context }: { node: LayoutComponentDefinition; context: LayoutContext }) {
const props = node.config ?? {}
const speed = props.speed ?? 10;
return (
<div className="relative overflow-hidden w-full h-full">
<div className={styles.ticker} style={{ animationDuration: `${speed}s` }} >
<DynamicLayoutRenderer nodes={props.items} context={context} />
</div>
</div>
)
}

View File

@ -0,0 +1,26 @@
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui";
import BannerNav from "./banner-nav";
import BannerCTA from "./banner-cta";
import BannerTicker from "./banner-ticker";
export default async function Banner({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) {
const props = nodes.config ?? {};
const bannerClassName = clx("relative h-8 mx-auto border-b duration-200 bg-white border-ui-border-base", props.className);
const bannerVariant = props.variant as "nav" | "cta" | "ticker";
if (!bannerVariant) return null;
const variants = {
"nav": BannerNav,
"cta": BannerCTA,
"ticker": BannerTicker,
};
const Component = variants[bannerVariant];
return (
<div className={bannerClassName}>
<Component node={nodes} context={context}/>
</div>
);
}

View File

@ -1,16 +1,17 @@
import { listCategories } from "@lib/data/categories"
import { listCollections } from "@lib/data/collections"
import { Text, clx } from "@medusajs/ui"
import { FooterProps } from "vibentec/component-props"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import MedusaCTA from "@modules/layout/components/medusa-cta"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
export default async function VtFooter({copyrightText}:FooterProps) {
export default async function VtFooter({ nodes, context }: { nodes?: LayoutComponentDefinition; context: LayoutContext }) {
const { collections } = await listCollections({
fields: "*products",
})
const productCategories = await listCategories()
const props = nodes?.config ?? {}
const copyrightText = props.copyrightText ?? "";
return (
<footer className="border-t border-ui-border-base w-full">

View File

@ -0,0 +1,14 @@
import { DynamicLayoutRenderer } from "vibentec/renderer"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
import { clx } from "@medusajs/ui";
export default function VtHeader({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) {
const { sticky = true } = nodes.config ?? {};
const cName = clx(sticky && "sticky top-0","inset-x-0 z-50 group");
return (
<header className={cName}>
{ nodes.children && <DynamicLayoutRenderer nodes={nodes.children} context={context} /> }
</header>
)
}

View File

@ -1,26 +1,22 @@
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 { DynamicLayoutRenderer } from "vibentec/renderer"
import { LayoutComponentDefinition, LayoutContext } from "vibentec/component-map";
export default async function VtNav({ nodes, context }: DynamicLayoutRendererProps) {
const regions = await listRegions().then((regions: StoreRegion[]) => regions)
export default function VtNav({ nodes, context }: { nodes: LayoutComponentDefinition; context: LayoutContext }) {
const props = nodes.config ?? {}
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">
<div className="h-full">
<SideMenu regions={regions} />
</div>
</div>
{nodes && (
<DynamicLayoutRenderer nodes={nodes} context={context} />
)}
</nav>
</header>
<div 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 items-center gap-x-4">
{props.left && <DynamicLayoutRenderer nodes={props.left} context={context} />}
</div>
<div className="flex items-center gap-x-4">
{props.center && <DynamicLayoutRenderer nodes={props.center} context={context} />}
</div>
<div className="flex items-center gap-x-4">
{props.right && <DynamicLayoutRenderer nodes={props.right} context={context} />}
</div>
</nav>
</div>
)
}
}

View File

@ -0,0 +1,12 @@
import LocalizedClientLink from "@modules/common/components/localized-client-link"
export default function SkeletonMegaMenu() {
return (
<LocalizedClientLink
className="hover:text-ui-fg-base hover:bg-neutral-100 rounded-full px-3 py-2"
href="/store"
>
Products
</LocalizedClientLink>
)
}

View File

@ -1,20 +1,25 @@
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 HomeButton from "@modules/layout/components/vt-homebutton"
import AccountButton from "@modules/layout/components/vt-accountbutton"
import VtCartButton from "@modules/layout/components/vt-cartbutton"
import VtHeader from "@modules/layout/templates/vt-header"
import Banner from "@modules/layout/templates/vt-banner"
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"
type ComponentConfig = Record<string, any>;
export interface LayoutComponentDefinition {
props?: Record<string, any>
config?: ComponentConfig
children?: LayoutComponentNode[]
}
//maps key = componentName to value = props + children
export type LayoutComponentNode = Record<string, LayoutComponentDefinition>
export interface LayoutContext {
customer: any;
cart: any;
@ -26,51 +31,41 @@ export type ComponentRenderer = {
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} />
// Utility methods
const configOnly = (Component: React.ComponentType<any>): ComponentRenderer => ({
render: (entry) => <Component {...entry.config} />
})
// Helper für Kinder-Rendering
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
// Component Map
export const componentMap: Record<string, ComponentRenderer> = {
Nav: {
render: (entry: any, ctx: LayoutContext) => ( <VtNav nodes={entry.children} context={ctx} /> ),
},
Div: {
render: (entry: any, ctx: LayoutContext) => (
<div {...entry.props}>
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
</div>
)
},
LocalizedClientLink: {
render: (entry: any) => (
<LocalizedClientLink {...entry.props}>{entry.props.label}</LocalizedClientLink>
)
},
CartButton: simple(CartButton),
Suspense: {
render: (entry: any, ctx: LayoutContext) => (
<React.Suspense
fallback={
entry.props?.fallback ? (
<DynamicLayoutRenderer nodes={entry.props.fallback} context={ctx} />
) : null
}
>
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
</React.Suspense>
),
},
CartMismatchBanner: simple(CartMismatchBanner),
FreeShippingPriceNudge: simple(FreeShippingPriceNudge),
Header: nodesContextRenderer(VtHeader),
Nav: nodesContextRenderer(VtNav),
VtMegaMenu: nodesContextRenderer(VtMegaMenu),
VtSideMenu: nodesContextRenderer(VtSideMenu),
Banner: nodesContextRenderer(Banner),
HomeButton: nodesContextRenderer(HomeButton),
AccountButton: nodesContextRenderer(AccountButton),
VtCartButton: nodesContextRenderer(VtCartButton),
Link: nodesContextRenderer(VtLink),
CartMismatchBanner: configOnly(CartMismatchBanner),
FreeShippingPriceNudge: configOnly(FreeShippingPriceNudge),
PropsChildren: {
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
},
Footer: simple(VtFooter),
}
Footer: nodesContextRenderer(VtFooter)
}
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]

View File

@ -1 +0,0 @@
export interface FooterProps { copyrightText?: string }

View File

@ -1,7 +1,8 @@
import fs from "fs"
import path from "path"
const fileName = "ste.medusa-starter.design.json";
//const fileName = "ste.medusa-starter.design.json";
const fileName = "ste.playground.design.json";
export async function loadDesignConfig() {
const filePath = path.join(process.cwd(), "config", fileName)

View File

@ -1,5 +1,5 @@
import React from "react"
import { LayoutComponentNode, LayoutContext, componentMap } from "./component-map"
import { ComponentName, LayoutComponentDefinition, LayoutComponentNode, LayoutContext, componentMap } from "./component-map"
export interface DynamicLayoutRendererProps {
nodes: LayoutComponentNode[]
@ -7,10 +7,22 @@ export interface DynamicLayoutRendererProps {
}
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>
const nodeArray = Array.isArray(nodes) ? nodes : [nodes];
return nodeArray.map((entry, index) => {
const [key, value] = Object.entries(entry)[0] as [ComponentName, LayoutComponentDefinition]
if (!value) {
console.warn(`[UI-Builder] Component definition is undefined: ${key}`);
return null;
}
const component = componentMap[key];
if (!component) {
console.warn(`[UI-Builder] Unknown component: ${key}`);
return null;
}
return <React.Fragment key={`${key}-${index}`}>{component.render(value, context)}</React.Fragment>
})
}

View File

@ -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: {

View File

@ -18,7 +18,8 @@
"paths": {
"@lib/*": ["lib/*"],
"@modules/*": ["modules/*"],
"@pages/*": ["pages/*"]
"@pages/*": ["pages/*"],
"@vibentec/*": ["vibentec/*"],
},
"plugins": [
{

793
yarn.lock

File diff suppressed because it is too large Load Diff