namds/refactor-base-layout #8

Merged
yen.nguyen merged 27 commits from namds/refactor-base-layout into main 2025-12-15 07:44:38 +00:00
6 changed files with 271 additions and 16 deletions
Showing only changes of commit f869d12c7a - Show all commits

View File

@ -20,22 +20,13 @@ export default function VtButton({
(CustomIcons as Record<string, any>)[iconName]
: undefined
return (
<>
{props?.icon && (
<IconButton className={props?.className ?? ""}>
{IconComponent && (
<IconComponent className={props?.iconClassName ?? ""} />
)}
{props?.label && (
<span className={props?.labelClassName ?? ""}>{props.label}</span>
)}
</IconButton>
<IconButton className={props?.className ?? ""}>
{IconComponent && (
<IconComponent className={props?.iconClassName ?? ""} />
)}
{!props?.icon && (
<Button className={props?.className ?? ""}>
{props?.label && <span>{props.label}</span>}
</Button>
{props?.label && (
<span className={props?.labelClassName ?? ""}>{props.label}</span>
)}
</>
</IconButton>
)
}

View File

@ -0,0 +1,114 @@
"use client"
import { Select } from "@medusajs/ui"
import ChevronDown from "@modules/common/icons/chevron-down"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import { useMemo, useState, useEffect } from "react"
import { useParams, usePathname, useRouter } from "next/navigation"
import ReactCountryFlag from "react-country-flag"
import { HttpTypes } from "@medusajs/types"
export default function VtCountrySelectClient({
nodes,
context,
regions,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
regions?: HttpTypes.StoreRegion[]
}) {
const props = nodes.config ?? {}
const triggerText = props?.trigger?.text
const [items, setItems] = useState<{ text: string; label?: string }[]>([])
const { countryCode } = useParams()
console.log(regions)
useEffect(() => {
if (!regions || regions.length === 0) {
setItems([])
return
}
const opts = regions
.map((r) =>
(r.countries || []).map((c) => ({
text: c.iso_2 ?? "",
label: c.display_name,
}))
)
.flat()
.filter((o) => o.text)
.sort((a, b) => (a.label ?? "").localeCompare(b.label ?? ""))
console.log(opts)
setItems(opts)
}, [regions])
const initialValue = (countryCode as string) || triggerText || ""
const [value, setValue] = useState(initialValue)
const pathname = usePathname()
const router = useRouter()
const handleChange = (next: string) => {
setValue(next)
if (!pathname || !next) return
const parts = pathname.split("/")
if (parts.length > 1) {
parts[1] = next.toLowerCase()
const newPath = parts.join("/")
router.replace(newPath)
}
}
const selectedItem = useMemo(() => {
return items.find((i) => i.text === value)
}, [items, value])
if (!triggerText && items.length === 0) {
return null
}
return (
<Select value={value} onValueChange={handleChange}>
<Select.Trigger
className={
(props.trigger?.className ?? "") +
"flex items-center gap-1 [&_svg:not(:first-of-type)]:hidden"
}
>
<span className="txt-compact-small flex items-center">
{/* @ts-ignore */}
{props.trigger?.isFlag && (
<ReactCountryFlag
svg
style={{
width: "16px",
height: "16px",
}}
countryCode={value ?? ""}
/>
)}
</span>
{selectedItem?.text.toUpperCase() || value} <ChevronDown />
</Select.Trigger>
<Select.Content>
{items.length > 0 &&
items.map((item: { text: string; label?: string }, index: number) => (
<Select.Item value={item.text || ""} key={item.text + index}>
<div className="flex items-center w-full gap-3">
{props.trigger?.isFlag && item.text && (
<ReactCountryFlag
svg
style={{
width: "16px",
height: "16px",
}}
countryCode={item.text ?? ""}
/>
)}
{item.text.toUpperCase()}
</div>
</Select.Item>
))}
</Select.Content>
</Select>
)
}

View File

@ -0,0 +1,22 @@
import { listRegions, getRegion } from "@lib/data/regions"
import { HttpTypes } from "@medusajs/types"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import VtCountryCodeSelectClient from "./index"
export default async function VtCountryCodeSelect({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const regions = (await listRegions()) as HttpTypes.StoreRegion[]
return (
<VtCountryCodeSelectClient nodes={nodes} context={context} regions={regions} />
)
}

View File

@ -0,0 +1,72 @@
"use client"
import { Select } from "@medusajs/ui"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import ChevronDown from "@modules/common/icons/chevron-down"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
export default function VtCurrencySelect({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
if (!props.trigger.text || props.items.length === 0) {
return null
}
return (
<Select>
<Select.Trigger
className={props.trigger.className + " flex items-center gap-1"}
>
{props.trigger.icon && (
<img
src={props.trigger.icon}
alt={props.trigger.text}
className="w-5 h-5 rounded-[50%] mr-3"
/>
)}
{props.trigger.text} {props.trigger.isShowArrow && <ChevronDown />}
</Select.Trigger>
<Select.Content>
{props.items.length > 0 &&
props.items.map(
(
item: {
text: string
className?: string
href?: string
icon?: string
},
index: number
) => (
<Select.Item
value={item.text || ""}
key={item.text + index}
className={item.className || ""}
>
{item.icon && (
<img
src={item.icon}
alt={item.text}
className="w-5 h-5 rounded-[50%] mr-3"
/>
)}
{item.href ? (
<LocalizedClientLink href={item.href}>
{item.text}
</LocalizedClientLink>
) : (
item.text
)}
</Select.Item>
)
)}
</Select.Content>
</Select>
)
}

View File

@ -21,7 +21,7 @@ export default function VtImage({
const props = (nodes.config as VtImageConfig) ?? {}
return (
<div className={clx("relative", props.className)}>
<Image src={props.src} alt={props.alt} fill objectFit={props.objectFit ?? "contain"} />
<img src={props.src} alt={props.alt} className={clx("w-full h-full object-cover", props.objectFit)} />
</div>
)
}

View File

@ -0,0 +1,56 @@
"use client"
import {
LayoutComponentDefinition,
LayoutContext,
} from "@vibentec/component-map"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import * as MedusaIcons from "@medusajs/icons"
import * as CustomIcons from "@modules/common/icons"
export default function VtMenuItem({
nodes,
context,
}: {
nodes: LayoutComponentDefinition
context: LayoutContext
}) {
const props = nodes.config ?? {}
const title = props.title ?? ""
const items = props.items ?? []
const icon = props.icon ?? ""
const getIconComponent = (icon: string | undefined) => {
return icon
? (MedusaIcons as Record<string, any>)[icon] ??
(CustomIcons as Record<string, any>)[icon]
: undefined
}
return (
<div className={props.className ?? "flex flex-col gap-y-2"}>
<span>{title}</span>
<ul className="grid grid-cols-1 gap-2" data-testid="footer-categories">
{items.map((item: { text: string; href?: string; icon?: string }, index: number) => {
const Icon = getIconComponent(item.icon)
return (
<li
key={`${item.text}-${index}`}
className={props.itemClassName ?? "text-ui-fg-subtle txt-small"}
>
{Icon && <Icon className={props.iconClassName ?? "inline-block mr-2"} />}
{item.href ? (
<LocalizedClientLink
className="hover:text-ui-fg-base"
href={item.href}
data-testid="category-link"
>
{item.text}
</LocalizedClientLink>
) : (
<span className="hover:text-ui-fg-base">{item.text}</span>
)}
</li>
)
})}
</ul>
</div>
)
}