refactor: add some component in footer and implement function for component

This commit is contained in:
Nam Doan 2025-12-11 12:59:00 +07:00
parent 9097a6e566
commit fbb13ae819
7 changed files with 486 additions and 559 deletions

View File

@ -47,7 +47,7 @@
"className": "h-24 bg-white text-[#003F31] gap-12", "className": "h-24 bg-white text-[#003F31] gap-12",
"left": [ "left": [
{ {
"Image": { "Logo": {
"config": { "config": {
"src": "/3bear-logo.png", "src": "/3bear-logo.png",
"alt": "MyShop", "alt": "MyShop",
@ -197,80 +197,33 @@
{ {
"Footer": { "Footer": {
"config": { "config": {
"className": "content-container flex w-full bg-[#003f31] text-white border justify-between pb-8 pt-14", "className": "content-container border-none flex w-full bg-[#003f31] text-white border justify-between pb-8 pt-14",
"leftClassName": "flex-col ml-3", "leftClassName": "flex-col ml-3",
"centerClassName": "", "centerClassName": "",
"rightClassName": "flex gap-[12rem] mr-[100px]", "rightClassName": "flex gap-[10rem] mr-[80px]",
"left": [ "left": [
{ {
"Image": { "VtFooterHero": {
"config": { "config": {
"src": "/3bear-white-logo.avif", "logoClassName": "h-[100px] w-[200px]",
"alt": "MyShop", "logoSrc": "/3bear-white-logo.avif",
"className": "h-full w-[150px] ml-10 pb-6", "logoAlt": "3Bear",
"objectFit": "contain" "title": "Melde dich für unsere Oatnews an 💛",
} "email": {
} "emailInputClassName": "w-[300px] ml-8"
},
{
"Text": {
"config": {
"label": "Melde dich für unsere Oatnews an 💛",
"className": "flex flex-col gap-y-2 ml-10 w-full text-[24px] font-semibold text-white"
}
}
},
{
"Input": {
"config": {
"placeholder": "E-mail",
"className": "w-[350px] mt-6 border border-gray-200 bg-transparent rounded-md h-[48px] px-[16px] text-[16px] leading-[125%] text-white flex font-bold ml-10"
}
}
},
{
"VtSocialLinks": {
"config": {
"className": "flex items-center gap-1 ml-6 mt-8"
},
"children": [
{
"Button": {
"config": {
"icon": "Twitter",
"iconClassName": "text-white",
"className": "w-[48px] h-[48px] shadow-none hover:bg-transparent bg-transparent text-white rounded-md flex items-center justify-center"
}
}
}, },
{ "socials": [
"Button": { { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"config": { { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"icon": "Twitter", { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
"iconClassName": "text-white", { "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }
"className": "w-[48px] h-[48px] shadow-none hover:bg-transparent bg-transparent text-white rounded-md flex items-center justify-center" ],
} "socialsClassName": "ml-8 mt-10",
} "className": "",
}, "ctaClassName": "ml-8",
{ "titleClassName": "ml-8 text-white w-full",
"Button": { "descriptionClassName": "ml-8"
"config": { }
"icon": "Twitter",
"iconClassName": "text-white",
"className": "w-[48px] h-[48px] shadow-none hover:bg-transparent bg-transparent text-white rounded-md flex items-center justify-center"
}
}
},
{
"Button": {
"config": {
"icon": "Twitter",
"iconClassName": "text-white",
"className": "w-[48px] h-[48px] shadow-none hover:bg-transparent bg-transparent text-white rounded-md flex items-center justify-center"
}
}
}
]
} }
} }
], ],
@ -341,7 +294,7 @@
"text": "B2B", "text": "B2B",
"href": "/categories/accessories" "href": "/categories/accessories"
}, },
{ {
"text": "Presse", "text": "Presse",
"href": "/categories/accessories" "href": "/categories/accessories"
} }
@ -352,5 +305,26 @@
] ]
} }
} }
},
{
"Footer": {
"config": {
"className": "content-container bg-[#003f31] w-full text text-[#11314E] flex items-center justify-between",
"leftClassName": "w-full",
"left": [
],
"center": [],
"right": [
{
"VtFooterBottom": {
"config": {
"className": " mr-[80px]",
"icons": ["Mastercard", "PayPal", "Visa", "Mastercard","Mastercard","Mastercard","Mastercard"]
}
}
}
]
}
}
} }
] ]

View File

@ -48,11 +48,11 @@
"className": "h-24 bg-[#1f3521] text-white gap-12", "className": "h-24 bg-[#1f3521] text-white gap-12",
"left": [ "left": [
{ {
"Image": { "Logo": {
"config": { "config": {
"src": "/drsquatch-logo.webp", "src": "/drsquatch-logo.webp",
"alt": "MyShop", "alt": "MyShop",
"className": "h-full w-[180px] mr-24", "className": "h-auto w-40 mr-24",
"objectFit": "contain" "objectFit": "contain"
} }
} }
@ -98,31 +98,12 @@
], ],
"right": [ "right": [
{ {
"Dropdown": { "VtCountryCodeSelect": {
"config": { "config": {
"trigger": { "trigger": {
"icon": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Flag_of_Germany.svg/1200px-Flag_of_Germany.svg.png", "className": "w-auto font-bold text-[13px] text-white flex justify-start items-center gap-1 hover:text-[#009b93] bg-transparent shadow-none hover:bg-transparent",
"text": "DE", "isFlag": true
"className": "font-bold text-[1rem] text-white 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": "DE",
"href": "/"
},
{
"icon": "https://cdn.shopify.com/s/files/1/0275/7784/3817/files/EU_Flag.svg?v=1652208424",
"text": "EU",
"href": "/"
},
{
"icon": "https://cdn.shopify.com/s/files/1/0275/7784/3817/files/Australia_Flag.svg?v=1657674627",
"text": "AU",
"href": "/"
}
]
} }
} }
}, },
@ -137,8 +118,8 @@
{ {
"VtCartButton": { "VtCartButton": {
"config": { "config": {
"variant": "cartIconButton", "icon": "ShoppingCart",
"className": "shadow-none bg-transparent text-white" "className": "shadow-none bg-transparent text-black w-[50px]"
} }
} }
} }
@ -169,445 +150,163 @@
{ {
"Footer": { "Footer": {
"config": { "config": {
"className": "content-container flex w-full bg-[#1f3521]", "className": "content-container border-none bg-[#1f3621] flex w-full border justify-between pb-8 gap-10 pt-16 px-[120px]",
"children": [ "leftClassName": "flex ml-6 gap-x-24",
"centerClassName": "flex",
"rightClassName": "flex",
"left": [
{ {
"Section": { "VtMenuItem": {
"config": { "config": {
"className": "grid grid-cols-2 w-full py-[40px]" "title": "Help",
}, "className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white gap-8",
"children": [ "itemClassName": "text-[14px] font-[400] mt-3",
{ "items": [
"Section": { {
"config": { "text": "FAQ",
"className": "flex mr-8 gap-1 w-full gap-6" "href": "/"
}, },
"children": [ {
{ "text": "Track my order",
"Section": { "href": "/categories/shoes"
"config": { },
"className": "flex mr-8 gap-1 w-full flex-col gap-6" {
}, "text": "Placeholder",
"children": [ "href": "/categories/accessories"
{ },
"Link": { {
"config": { "text": "Placeholder",
"label": "Help", "href": "/categories/accessories"
"href": "/", },
"className": "text-[16px] leading-[125%] text-white flex hover:text-white font-bold" {
} "text": "Placeholder",
} "href": "/categories/accessories"
}, },
{ {
"Link": { "text": "Placeholder",
"config": { "href": "/categories/accessories"
"label": "FAQ",
"href": "/",
"className": "text-[14px] leading-[160%] text-white flex mr-8 gap-1 hover:bg-transparent hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Track My Order",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Store policies",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Contact us",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
}
]
}
},
{
"Section": {
"config": {
"className": "flex mr-8 gap-1 w-full flex-col gap-6"
},
"children": [
{
"Link": {
"config": {
"label": "Shop",
"href": "/",
"className": "text-[16px] leading-[125%] text-white flex hover:text-white font-bold"
}
}
},
{
"Link": {
"config": {
"label": "FAQ",
"href": "/",
"className": "text-[14px] leading-[160%] text-white flex mr-8 gap-1 hover:bg-transparent hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Track My Order",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Store policies",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Contact us",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
}
]
}
},
{
"Section": {
"config": {
"className": "flex mr-8 gap-1 w-full flex-col gap-6"
},
"children": [
{
"Link": {
"config": {
"label": "Info",
"href": "/",
"className": "text-[16px] leading-[125%] text-white flex hover:text-white font-bold"
}
}
},
{
"Link": {
"config": {
"label": "FAQ",
"href": "/",
"className": "text-[14px] leading-[160%] text-white flex mr-8 gap-1 hover:bg-transparent hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Track My Order",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Store policies",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Contact us",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
},
{
"Link": {
"config": {
"label": "Placeholder",
"href": "/",
"className": "text-[14px] leading-[125%] text-white flex hover:text-white font-extralight"
}
}
}
]
}
},
{
"Section": {
"config": {
"className": "flex mr-8 gap-1 w-full flex-col gap-6"
},
"children": [
{
"Image": {
"config": {
"src": "/b-corp-logo.webp",
"alt": "B Corp Logo",
"className": "w-[96px] h-[118px]"
}
}
}
]
}
}
]
} }
}, ]
{ }
"Section": {
"config": {
"className": "flex mr-8 gap-1 w-full flex-col gap-6"
},
"children": [
{
"Text": {
"config": {
"label": "Don't miss out on hot deals! Sign up and get up to 30% off.",
"className": "text-[16px] leading-[125%] text-white flex font-bold"
}
}
},
{
"Section": {
"config": {
"className": "flex w-full flex gap-6"
},
"children": [
{
"Input": {
"config": {
"placeholder": "Enter your email address",
"className": "w-[75%] rounded-md h-[48px] px-[16px] text-[16px] leading-[125%] text-white flex font-bold"
}
}
},
{
"Button": {
"config": {
"label": "Sign up",
"className": "w-[25%] rounded-md bg-[#cc6328] h-[48px] px-[16px] text-[16px] leading-[125%] text-white flex font-bold"
}
}
}
]
}
},
{
"Section": {
"config": {
"className": "flex w-full flex"
},
"children": [
{
"Button": {
"config": {
"icon": "Twitter",
"className": "text-white flex justify-start gap-1 shadow-none w-[50px] bg-transparent hover:bg-transparent"
}
}
},
{
"Button": {
"config": {
"icon": "Twitter",
"className": "text-white flex justify-start gap-1 shadow-none w-[50px] bg-transparent hover:bg-transparent"
}
}
},
{
"Button": {
"config": {
"icon": "Twitter",
"className": "text-white flex justify-start gap-1 shadow-none w-[50px] bg-transparent hover:bg-transparent"
}
}
},
{
"Button": {
"config": {
"icon": "Twitter",
"className": "text-white flex justify-start gap-1 shadow-none w-[50px] bg-transparent hover:bg-transparent"
}
}
},
{
"Button": {
"config": {
"icon": "Twitter",
"className": "text-white flex justify-start gap-1 shadow-none w-[50px] bg-transparent hover:bg-transparent"
}
}
}
]
}
}
]
}
},
{
"Section": {
"config": {
"className": "flex justify-center col-span-full w-full gap-1"
},
"children": [
{
"Text": {
"config": {
"label": "DR. SQUATCH is a registered trademark of Dr. Squatch, LLC © 2025, Dr. Squatch, LLC All rights reserved.",
"className": "text-[11px] mt-[3rem] leading-[125%] text-white flex"
}
}
},
{
"Text":{
"config":{
"label":"Terms of Use",
"className":"text-[11px] mt-[3rem] leading-[125%] text-orange-500 flex"
}
}
},
{
"Text":{
"config":{
"label":"|",
"className":"text-[11px] mt-[3rem] leading-[125%] text-white flex"
}
}
},
{
"Text":{
"config":{
"label":"Privacy Policy",
"className":"text-[11px] mt-[3rem] leading-[125%] text-orange-500 flex"
}
}
}
]
}
}
]
} }
},
{
"VtMenuItem": {
"config": {
"title": "Shop",
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] flex items-center mt-3",
"items": [
{
"text": "Twitter",
"href": "/"
},
{
"text": "Facebook",
"href": "/categories/shoes"
},
{
"text": "Pinterest",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
},
{
"text": "Placeholder",
"href": "/categories/accessories"
}
]
}
}
},
{
"VtMenuItem": {
"config": {
"title": "Info",
"className": "flex flex-col gap-y-2 text-[16px] font-semibold text-white",
"itemClassName": "text-[14px] font-[400] w-[200px] mt-3",
"items": [
{
"text": "The Squatch Difference",
"href": "/"
},
{
"text": "Why Natural Products",
"href": "/categories/shoes"
},
{
"text": "No Harmful Ingredients",
"href": "/categories/accessories"
}
]
}
}
}
],
"center": [
{
"Logo": {
"config": {
"src": "/b-corp-logo.webp",
"alt": "MyShop",
"className": "h-auto w-[90px] mr-24",
"objectFit": "contain"
}
}
}
],
"right": [
{
"VtFooterSignUp": {
"config": {
"title": "Don't miss out on hot deals! Sign up and get up to 30% off.",
"className": "max-w-[760px]",
"titleClassName": "text-white text-[18px]",
"formClassName": "mt-2 w-full",
"inputClassName": "w-full",
"buttonClassName": "bg-[#C4622C] w-[90px]",
"socialsClassName": "mt-4 gap-8",
"socials": [
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" },
{ "icon": "Twitter", "href": "/", "className": "w-5 h-5 text-white" }
]
}
}
}
]
}
}
},
{
"VtFooterBottom": {
"config": {
"className": "text-white text-[14px] font-[400] bg-[#1f3621] flex items-center justify-center",
"text": "©2025 Vibentec IT. All rights reserved",
"linksClassName": "flex items-center text-orange-500 mt-2 pl-2",
"links": [
{
"label": "Privacy Policy",
"href": "/"
},
{
"label": "Terms of Service",
"href": "/categories/shoes"
},
{
"label": "Cookie Policy",
"href": "/categories/accessories"
} }
] ]
} }

View File

@ -25,17 +25,17 @@ export default async function VtFooter({
return ( return (
<footer className={props?.className ?? ""}> <footer className={props?.className ?? ""}>
{props?.left && ( {props?.left && (
<div className={clx("flex gap-x-4 h-full", props?.leftClassName)}> <div className={clx("flex h-full", props?.leftClassName)}>
<DynamicLayoutRenderer nodes={props?.left} context={context} /> <DynamicLayoutRenderer nodes={props?.left} context={context} />
</div> </div>
)} )}
{props?.center && ( {props?.center && (
<div className={clx("flex gap-x-4 h-full", props?.centerClassName)}> <div className={clx("flex h-full", props?.centerClassName)}>
<DynamicLayoutRenderer nodes={props?.center} context={context} /> <DynamicLayoutRenderer nodes={props?.center} context={context} />
</div> </div>
)} )}
{props?.right && ( {props?.right && (
<div className={clx("flex gap-x-4 h-full", props?.rightClassName)}> <div className={clx("flex h-full", props?.rightClassName)}>
<DynamicLayoutRenderer nodes={props?.right} context={context} /> <DynamicLayoutRenderer nodes={props?.right} context={context} />
</div> </div>
)} )}

View File

@ -1,6 +1,10 @@
import * as MedusaIcons from "@medusajs/icons" import * as MedusaIcons from "@medusajs/icons"
import { clx } from "@medusajs/ui" import { clx } from "@medusajs/ui"
import { LayoutComponentDefinition, LayoutContext } from "@vibentec/component-map" import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
export default function VtFooterBottom({ export default function VtFooterBottom({
nodes, nodes,
@ -9,23 +13,62 @@ export default function VtFooterBottom({
nodes: LayoutComponentDefinition nodes: LayoutComponentDefinition
context: LayoutContext context: LayoutContext
}) { }) {
const props = (nodes.config) ?? {} const props = nodes.config ?? {}
const renderIcon = (name: string, idx: number) => { const renderIcon = (name: string, idx: number) => {
const IconComp = (MedusaIcons as Record<string, any>)[name] const IconComp = (MedusaIcons as Record<string, any>)[name]
if (!IconComp) return null if (!IconComp) return null
return <IconComp key={`${name}-${idx}`} className="shadow-none" />
}
const renderLink = (item: any, idx: number, total: number) => {
const LinkEl = (
<LocalizedClientLink
href={item?.href ?? "#"}
className={clx(
"hover:underline",
props.linkClassName
)}
>
{item?.label ?? ""}
</LocalizedClientLink>
)
return ( return (
<IconComp key={`${name}-${idx}`} className="shadow-none" /> <span
key={`link-${idx}`}
className={clx("flex items-center", props.linkItemClassName)}
>
{LinkEl}
{idx < total - 1 && (
<span className={clx("mx-2 text-white", props.separatorClassName)}>|</span>
)}
</span>
) )
} }
return ( return (
<div className={clx("flex w-full items-center justify-between", props.className)}> <div
<span className={clx("text-[14px] font-[400] pt-2", props.textClassName)}>{props.text}</span> className={clx(
<div className={clx("flex items-center gap-2", props.iconsClassName)}> "flex w-full items-center justify-between",
{(props.icons ?? []).map(renderIcon)} props.className
)}
>
<span className={clx("text-[14px] font-[400] pt-2", props.textClassName)}>
{props.text}
</span>
<div className={clx("flex items-center gap-4")}>
{props.links && props.links.length > 0 && (
<div className={clx("flex items-center", props.linksClassName)}>
{props.links.map((l: any, idx: number) =>
renderLink(l, idx, props.links.length)
)}
</div>
)}
<div className={clx("flex items-center gap-2", props.iconsClassName)}>
{(props.icons ?? []).map(renderIcon)}
</div>
</div> </div>
</div> </div>
) )
} }

View File

@ -1,19 +1,18 @@
import { clx } from "@medusajs/ui" import { clx } from "@medusajs/ui"
import { ChevronRightMini } from "@medusajs/icons" import { ChevronRightMini } from "@medusajs/icons"
import { LayoutComponentDefinition, LayoutContext } from "@vibentec/component-map" import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import * as MedusaIcons from "@medusajs/icons"
import * as CustomIcons from "@modules/common/icons"
import LocalizedClientLink from "@modules/common/components/localized-client-link" import LocalizedClientLink from "@modules/common/components/localized-client-link"
interface VtFooterHeroConfig { interface SocialIcon {
logoSrc: string href?: string
logoAlt?: string icon?: string
title: string imgSrc?: string
description: string
cta?: { label: string; href: string }
className?: string className?: string
logoClassName?: string
titleClassName?: string
descriptionClassName?: string
ctaClassName?: string
} }
export default function VtFooterHero({ export default function VtFooterHero({
@ -23,8 +22,49 @@ export default function VtFooterHero({
nodes: LayoutComponentDefinition nodes: LayoutComponentDefinition
context: LayoutContext context: LayoutContext
}) { }) {
const props = (nodes.config as VtFooterHeroConfig) ?? {} const props = nodes.config ?? {}
const IconComp = (name?: string) => {
if (!name) return null
return (
(MedusaIcons as Record<string, any>)[name] ||
(CustomIcons as Record<string, any>)[name] ||
null
)
}
const renderSocialContent = (item: SocialIcon) => {
const Icon = IconComp(item.icon)
if (Icon) return <Icon className={item.className} />
if (item.imgSrc)
return (
<img
src={item.imgSrc}
alt={item.icon ?? "social"}
className={item.className}
/>
)
return (
<span className="w-4 h-4 rounded-md bg-white/10 text-white flex items-center justify-center text-sm">
{item.icon?.[0] ?? "?"}
</span>
)
}
const renderSocialIcon = (icon: SocialIcon, idx: number) => {
const content = renderSocialContent(icon)
return icon.href ? (
<a
key={idx}
href={icon.href}
className={clx("hover:opacity-80", icon.className)}
>
{content}
</a>
) : (
<span key={idx} className={clx("", icon.className)}>
{content}
</span>
)
}
return ( return (
<div className={clx("flex flex-col items-start", props.className)}> <div className={clx("flex flex-col items-start", props.className)}>
{props.logoSrc && ( {props.logoSrc && (
@ -35,12 +75,22 @@ export default function VtFooterHero({
/> />
)} )}
{props.title && ( {props.title && (
<h2 className={clx("mt-4 w-[320px] text-[24px] font-semibold text-[#11314E]", props.titleClassName)}> <h2
className={clx(
"mt-4 w-[320px] text-[24px] font-semibold text-[#11314E]",
props.titleClassName
)}
>
{props.title} {props.title}
</h2> </h2>
)} )}
{props.description && ( {props.description && (
<p className={clx("mt-2 text-ui-fg-subtle txt-small", props.descriptionClassName)}> <p
className={clx(
"mt-2 text-ui-fg-subtle txt-small",
props.descriptionClassName
)}
>
{props.description} {props.description}
</p> </p>
)} )}
@ -56,7 +106,53 @@ export default function VtFooterHero({
<ChevronRightMini className="order-1" /> <ChevronRightMini className="order-1" />
</LocalizedClientLink> </LocalizedClientLink>
)} )}
{props.email && (
<form
className={clx(
"mt-4 w-full max-w-[500px]",
props.email?.emailInputClassName
)}
>
<div className="relative flex items-center justify-between border border-white/40 rounded-md px-4 py-3">
<input
id="vt-footer-hero-email"
type="email"
name="contact[email]"
placeholder=" "
autoComplete="email"
required
className="peer bg-transparent outline-none text-white/90 placeholder-transparent flex-1"
/>
<label
htmlFor="vt-footer-hero-email"
className="absolute left-4 text-white/60 pointer-events-none transition-all duration-300
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:text-white/50
peer-focus:top-3 peer-focus:-translate-y-2 peer-focus:left-3 peer-focus:text-white
peer-[:not(:placeholder-shown)]:translate-y-[-0.85rem]"
>
E-Mail
</label>
<button
type="submit"
className="ml-4 w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center"
>
<span className="sr-only">Abonnieren</span>
<ChevronRightMini />
</button>
</div>
</form>
)}
{props.socials && props.socials.length > 0 ? (
<div
className={clx(
"flex items-center gap-4 mt-2",
props.socialsClassName
)}
>
{props.socials.map(renderSocialIcon)}
</div>
) : null}
</div> </div>
) )
} }

View File

@ -0,0 +1,114 @@
"use client"
import { Button, Input } from "@medusajs/ui"
import { clx } from "@medusajs/ui"
import * as MedusaIcons from "@medusajs/icons"
import * as CustomIcons from "@modules/common/icons"
import { LayoutComponentDefinition, LayoutContext } from "@vibentec/component-map"
import React, { useState } from "react"
interface SocialIcon {
href?: string
icon?: string
imgSrc?: string
className?: string
}
interface VtFooterSignupConfig {
title?: string
placeholder?: string
buttonLabel?: string
className?: string
titleClassName?: string
formClassName?: string
inputClassName?: string
buttonClassName?: string
socialsClassName?: string
socials?: SocialIcon[]
}
export default function VtFooterSignUp({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = (nodes.config as VtFooterSignupConfig) ?? {}
const [email, setEmail] = useState("")
const IconComp = (name?: string) => {
if (!name) return null
return (
(MedusaIcons as Record<string, any>)[name] ||
(CustomIcons as Record<string, any>)[name] ||
null
)
}
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log("newsletter_signup", { email })
}
const renderSocialContent = (item: SocialIcon) => {
const Icon = IconComp(item.icon)
if (Icon) return <Icon className={item.className} />
if (item.imgSrc) return <img src={item.imgSrc} alt={item.icon ?? "social"} className={item.className} />
return (
<span className="w-4 h-4 rounded-md bg-white/10 text-white flex items-center justify-center text-sm">
{item.icon?.[0] ?? "?"}
</span>
)
}
const renderSocialIcon = (icon: SocialIcon, idx: number) => {
const content = renderSocialContent(icon)
return icon.href ? (
<a key={idx} href={icon.href} className={clx("hover:opacity-80", icon.className)}>
{content}
</a>
) : (
<span key={idx} className={clx("", icon.className)}>
{content}
</span>
)
}
return (
<div className={clx("flex flex-col gap-4", props.className)}>
{props.title && (
<p className={clx("text-white text-[18px]", props.titleClassName)}>
{props.title}
</p>
)}
<form onSubmit={onSubmit} className={clx("flex items-center gap-4", props.formClassName)}>
<input
type="email"
placeholder={props.placeholder ?? "Email"}
value={email}
onChange={(e) => setEmail(e.target.value)}
className={clx(
"h-[48px] rounded-lg bg-white text-black px-4",
props.inputClassName
)}
/>
<Button
type="submit"
className={clx(
"h-[48px] rounded-lg bg-[#C4622C] text-white",
props.buttonClassName
)}
>
{props.buttonLabel ?? "Sign Up"}
</Button>
</form>
{props.socials && props.socials.length > 0 ? (
<div className={clx("flex items-center gap-4 mt-2", props.socialsClassName)}>
{props.socials.map(renderSocialIcon)}
</div>
) : null}
</div>
)
}

View File

@ -14,7 +14,6 @@ import VtLink from "@modules/layout/components/vt-linkbutton"
import VtSideMenu from "@modules/layout/components/vt-sidemenu" import VtSideMenu from "@modules/layout/components/vt-sidemenu"
import VtDropdown from "@modules/layout/templates/vt-dropdown" import VtDropdown from "@modules/layout/templates/vt-dropdown"
import VtSearchInput from "@modules/layout/templates/vt-search-input" import VtSearchInput from "@modules/layout/templates/vt-search-input"
import VtSection from "@modules/layout/templates/vt-section"
import VtCurrencySelect from "@modules/layout/templates/vt-currency-select" import VtCurrencySelect from "@modules/layout/templates/vt-currency-select"
import VtMenuItem from "@modules/layout/templates/vt-menu-item" import VtMenuItem from "@modules/layout/templates/vt-menu-item"
import VtCountryCodeSelect from "@modules/layout/templates/vt-country-select/server" import VtCountryCodeSelect from "@modules/layout/templates/vt-country-select/server"
@ -22,6 +21,7 @@ import VtSocialLinks from "@modules/layout/templates/vt-social-link"
import VtFooterHero from "@modules/layout/templates/vt-footer/vt-footer-hero" import VtFooterHero from "@modules/layout/templates/vt-footer/vt-footer-hero"
import VtFooterBottom from "@modules/layout/templates/vt-footer/vt-footer-bottom" import VtFooterBottom from "@modules/layout/templates/vt-footer/vt-footer-bottom"
import VtLogo from "@modules/layout/templates/vt-logo" import VtLogo from "@modules/layout/templates/vt-logo"
import VtFooterSignUp from "@modules/layout/templates/vt-footer/vt-footer-signup"
type ComponentConfig = Record<string, any>; type ComponentConfig = Record<string, any>;
@ -79,6 +79,7 @@ export const componentMap: Record<string, ComponentRenderer> = {
}, },
VtFooterHero: nodesContextRenderer(VtFooterHero), VtFooterHero: nodesContextRenderer(VtFooterHero),
VtFooterBottom: nodesContextRenderer(VtFooterBottom), VtFooterBottom: nodesContextRenderer(VtFooterBottom),
VtFooterSignUp: nodesContextRenderer(VtFooterSignUp),
Footer: nodesContextRenderer(VtFooter) Footer: nodesContextRenderer(VtFooter)
} }