namds/refactor-base-layout #8
|
|
@ -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"} } }
|
||||
]
|
||||
|
|
@ -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"} } }
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
|
yen.nguyen marked this conversation as resolved
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from "react";
|
||||
|
yen.nguyen marked this conversation as resolved
yen.nguyen
commented
same problem as with /vt-about, Is it for testing purpose only? same problem as with /vt-about, Is it for testing purpose only?
namds29
commented
this is for testing purpose, i'll refactor later when we do this page this is for testing purpose, i'll refactor later when we do this page
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
yen.nguyen marked this conversation as resolved
yen.nguyen
commented
Need data binding improvement Need data binding improvement
namds29
commented
this part is component of medusajs we just clone it follow folder templates/nav/index. the data binding already implement below your highlight code, this is only the fallback when having any error of data response so will return this component this part is component of medusajs we just clone it follow folder templates/nav/index. the data binding already implement below your highlight code, this is only the fallback when having any error of data response so will return this component
|
||||
export default VtCartButton
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
@ -1 +0,0 @@
|
|||
export interface FooterProps { copyrightText?: string }
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
})
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
"paths": {
|
||||
"@lib/*": ["lib/*"],
|
||||
"@modules/*": ["modules/*"],
|
||||
"@pages/*": ["pages/*"]
|
||||
"@pages/*": ["pages/*"],
|
||||
"@vibentec/*": ["vibentec/*"],
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
This file is a route --> file name should be /about instead of /vt-about?
this is for testing purpose, i'll refactor later when we do this page