Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1716ef2cf4 | |||
| c800f87ffe |
@@ -474,6 +474,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"VtBrand": {
|
||||
"config": {
|
||||
"className": "w-full py-12 bg-[#CFECD9]",
|
||||
"innerClassName": "content-container flex flex-col items-center",
|
||||
"title": "Trusted By",
|
||||
"titleClassName": "text-[#003F31] text-[20px] font-semibold mb-8",
|
||||
"brandsClassName": "flex w-full items-center justify-between gap-12",
|
||||
"items": [
|
||||
{ "label": "Men'sHealth", "containerClassName": "", "className": "text-[#003F31] text-[36px] font-semibold italic" },
|
||||
{ "label": "GQ", "containerClassName": "", "className": "text-[#003F31] text-[44px] font-black tracking-tight" },
|
||||
{ "label": "BIRCHBOX", "containerClassName": "", "className": "text-[#003F31] text-[36px] font-semibold tracking-[0.2em]" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"FreeShippingPriceNudge": {
|
||||
"config": {
|
||||
|
||||
@@ -348,6 +348,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"VtBrand": {
|
||||
"config": {
|
||||
"className": "w-full py-12 bg-[#CFECD9]",
|
||||
"innerClassName": "content-container flex flex-col items-center",
|
||||
"title": "",
|
||||
"titleClassName": "text-[#1f3521] text-[20px] font-bold mb-8",
|
||||
"brandsClassName": "flex w-full items-center justify-between gap-12",
|
||||
"items": [
|
||||
{ "imageSrc": "/brand-logo.png", "alt": "Men's Health", "containerClassName": "", "imageClassName": "h-[40px] object-contain" },
|
||||
{ "imageSrc": "/brand-logo.png", "alt": "GQ", "containerClassName": "", "imageClassName": "h-[40px] object-contain" },
|
||||
{ "imageSrc": "/brand-logo.png", "alt": "Birchbox", "containerClassName": "", "imageClassName": "h-[40px] object-contain" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"VtFeedback": {
|
||||
"config": {
|
||||
"title": "100,000+ Reviews From Squatchers",
|
||||
"className": "content-container py-16",
|
||||
"titleClassName": "text-[#1f3521] text-[28px] font-bold text-center mb-10",
|
||||
"duration": 5,
|
||||
"options": { "loop": true },
|
||||
"itemClassName": "min-w-full px-6",
|
||||
"starsClassName": "text-[#C4622C] text-xl leading-none",
|
||||
"reviewTitleClassName": "text-[#1f3521] font-bold",
|
||||
"reviewTextClassName": "text-[#1f3521]",
|
||||
"authorClassName": "italic text-[#1f3521]",
|
||||
"controls": "mt-6 flex items-center justify-center gap-4",
|
||||
"items": [
|
||||
{
|
||||
"rating": 5,
|
||||
"title": "Ah-freaking-amazing!",
|
||||
"text": "So I just had my first shower with Dr. Squatch Cool Fresh Aloe. Holy sh*t this stuff is Ah-freaking-amazing! Talk about a life hack!",
|
||||
"author": "Stephen B."
|
||||
},
|
||||
{
|
||||
"rating": 5,
|
||||
"title": "Best damn soap ever…period.",
|
||||
"text": "Best Damn Soap I EVER bought! Super smooth on the skin, smells awesome, makes you feel good showering, and yes…the wife approves.",
|
||||
"author": "Chris H."
|
||||
},
|
||||
{
|
||||
"rating": 5,
|
||||
"title": "Hilarious…products awesome too",
|
||||
"text": "Ok…the Dr. Squatch commercials are just freakin hilarious…plus the products are awesome too! So yes, buy it now and subscribe to it!",
|
||||
"author": "Mike C."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "CartMismatchBanner": { "config": { "show": true } } },
|
||||
{ "FreeShippingPriceNudge": { "config": { "variant": "popup" } } }
|
||||
],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,72 @@
|
||||
import { clx } from "@medusajs/ui"
|
||||
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
||||
import {
|
||||
LayoutComponentDefinition,
|
||||
LayoutContext,
|
||||
} from "@vibentec/component-map"
|
||||
|
||||
export default async function VtBrand({
|
||||
nodes,
|
||||
context,
|
||||
}: {
|
||||
nodes: LayoutComponentDefinition
|
||||
context: LayoutContext
|
||||
}) {
|
||||
const props = nodes.config ?? {}
|
||||
|
||||
const title: string = props.title ?? "Trusted By"
|
||||
const items = props.items ?? []
|
||||
|
||||
const classes = {
|
||||
container: props.className ?? "w-full py-12 bg-[#CFECD9]",
|
||||
inner: props.innerClassName ?? "content-container flex flex-col items-center",
|
||||
title: props.titleClassName ?? "text-[#1f3521] text-[20px] font-bold mb-8",
|
||||
brands: props.brandsClassName ?? "flex w-full items-center justify-between gap-12",
|
||||
item: props.itemClassName ?? "opacity-90",
|
||||
image: props.imageClassName ?? "h-[48px] w-auto object-contain",
|
||||
label: props.labelClassName ?? "text-[#1f3521] text-[36px] font-semibold",
|
||||
}
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const renderItem = (brand: any, idx: number) => {
|
||||
const content = brand.imageSrc ? (
|
||||
<img
|
||||
src={brand.imageSrc}
|
||||
alt={brand.alt ?? brand.label ?? `brand-${idx}`}
|
||||
className={clx(classes.image, brand.imageClassName)}
|
||||
/>
|
||||
) : (
|
||||
<span className={clx(classes.label, brand.className)}>
|
||||
{brand.label ?? ""}
|
||||
</span>
|
||||
)
|
||||
|
||||
return brand.href ? (
|
||||
<LocalizedClientLink
|
||||
key={`brand-${idx}`}
|
||||
href={brand.href}
|
||||
className={clx(classes.item, brand.containerClassName)}
|
||||
>
|
||||
{content}
|
||||
</LocalizedClientLink>
|
||||
) : (
|
||||
<div className={clx(classes.item, brand.containerClassName)} key={`brand-${idx}`}>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classes.container}>
|
||||
<div className={classes.inner}>
|
||||
{title && <div className={classes.title}>{title}</div>}
|
||||
<div className={classes.brands}>
|
||||
{items.map((brand: any, idx: number) => renderItem(brand, idx))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
import useEmblaCarousel from "embla-carousel-react"
|
||||
import Autoplay from "embla-carousel-autoplay"
|
||||
import { useMemo } from "react"
|
||||
import { clx } from "@medusajs/ui"
|
||||
import {
|
||||
LayoutComponentDefinition,
|
||||
LayoutContext,
|
||||
} from "@vibentec/component-map"
|
||||
import { NextButton, PrevButton, usePrevNextButtons } from "@modules/layout/templates/vt-carousel/carousel-arrow-button"
|
||||
|
||||
export default function VtFeedback({
|
||||
nodes,
|
||||
context,
|
||||
}: {
|
||||
nodes: LayoutComponentDefinition
|
||||
context: LayoutContext
|
||||
}) {
|
||||
|
||||
const props = nodes.config ?? {}
|
||||
|
||||
const title: string = props.title ?? ""
|
||||
const items = props.items ?? []
|
||||
const durationSeconds: number = props.duration ?? 5
|
||||
const options = props.options ?? { loop: true }
|
||||
const plugins = useMemo(() => {
|
||||
if (!durationSeconds || durationSeconds <= 0) return []
|
||||
return [
|
||||
Autoplay({
|
||||
delay: durationSeconds * 1000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
]
|
||||
}, [durationSeconds])
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel(options, plugins)
|
||||
const { prevBtnDisabled, nextBtnDisabled, onPrevButtonClick, onNextButtonClick } = usePrevNextButtons(emblaApi)
|
||||
|
||||
const classes = {
|
||||
container: props.className ?? "content-container py-16",
|
||||
title: props.titleClassName ?? "text-[#1f3521] text-[28px] font-bold text-center mb-10",
|
||||
viewport: "relative overflow-hidden",
|
||||
containerInner: "flex",
|
||||
slide: props.itemClassName ?? "min-w-full px-6",
|
||||
slideInner: "flex flex-col items-center text-center gap-3",
|
||||
stars: props.starsClassName ?? "text-[#C4622C] text-xl leading-none",
|
||||
reviewTitle: props.reviewTitleClassName ?? "text-[#1f3521] font-bold",
|
||||
reviewText: props.reviewTextClassName ?? "text-[#1f3521]",
|
||||
author: props.authorClassName ?? "italic text-[#1f3521]",
|
||||
controls: props.controls,
|
||||
}
|
||||
|
||||
if (!items || items.length === 0) return null
|
||||
|
||||
const showControls = items.length > 1 && classes.controls
|
||||
|
||||
const renderStars = (rating?: number) => {
|
||||
const count = Math.max(0, Math.min(5, Math.round(rating ?? 5)))
|
||||
return "★★★★★".slice(0, count)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classes.container}>
|
||||
{title && <h2 className={classes.title}>{title}</h2>}
|
||||
<div className={classes.viewport} ref={emblaRef}>
|
||||
<div className={classes.containerInner}>
|
||||
{items.map((it: any, idx: number) => (
|
||||
<div className={classes.slide} key={`feedback-${idx}`}>
|
||||
<div className={classes.slideInner}>
|
||||
<div className={classes.stars}>{renderStars(it.rating)}</div>
|
||||
{it.title && <div className={classes.reviewTitle}>{it.title}</div>}
|
||||
{it.text && <div className={classes.reviewText}>{it.text}</div>}
|
||||
{it.author && <div className={classes.author}>{it.author}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showControls && (
|
||||
<div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 flex items-center justify-between px-4">
|
||||
<div className="pointer-events-auto">
|
||||
<PrevButton onClick={onPrevButtonClick} disabled={prevBtnDisabled} />
|
||||
</div>
|
||||
<div className="pointer-events-auto">
|
||||
<NextButton onClick={onNextButtonClick} disabled={nextBtnDisabled} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import { VtCarousel } from "@modules/layout/templates/vt-carousel"
|
||||
import { VtCtaBanner } from "@modules/layout/templates/vt-cta-banner"
|
||||
import VtFeaturedProducts from "@modules/home/components/vt-featured-products"
|
||||
import VtCategoryHighlight from "@modules/home/components/vt-category-highlight"
|
||||
import VtBrand from "@modules/home/components/vt-brand"
|
||||
import VtFeedback from "@modules/home/components/vt-feedback"
|
||||
|
||||
type ComponentConfig = Record<string, any>
|
||||
|
||||
@@ -103,6 +105,8 @@ export const componentMap: Record<string, ComponentRenderer> = {
|
||||
ImageDisplayer: nodesContextRenderer(VtCarousel),
|
||||
VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts),
|
||||
VtCategoryHighlight: nodesContextRenderer(VtCategoryHighlight),
|
||||
VtBrand: nodesContextRenderer(VtBrand),
|
||||
VtFeedback: nodesContextRenderer(VtFeedback),
|
||||
}
|
||||
|
||||
export type ComponentName = keyof typeof componentMap
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from "fs"
|
||||
import path from "path"
|
||||
import { jsonFileNames } from "./devJsonFileNames"
|
||||
|
||||
const fileName = jsonFileNames.nam3Bear
|
||||
const fileName = jsonFileNames.namDrsquatch
|
||||
|
||||
async function readDesignFile() {
|
||||
const filePath = path.join(process.cwd(), "config", fileName)
|
||||
@@ -10,6 +10,7 @@ async function readDesignFile() {
|
||||
return JSON.parse(fileData)
|
||||
}
|
||||
|
||||
|
||||
export async function loadLayoutConfig() {
|
||||
const config = await readDesignFile()
|
||||
if (Array.isArray(config)) return config
|
||||
|
||||
Reference in New Issue
Block a user