From 1716ef2cf4e05befb9dedd70afdb3973b9fe4132 Mon Sep 17 00:00:00 2001 From: Nam Doan Date: Mon, 5 Jan 2026 09:49:35 +0700 Subject: [PATCH] feat: implement vt feedback component --- config/nam.drsquatch.design.json | 37 ++++++++ .../home/components/vt-feedback/index.tsx | 91 +++++++++++++++++++ src/vibentec/component-map.tsx | 2 + 3 files changed, 130 insertions(+) create mode 100644 src/modules/home/components/vt-feedback/index.tsx diff --git a/config/nam.drsquatch.design.json b/config/nam.drsquatch.design.json index 84a0dda..72c3150 100644 --- a/config/nam.drsquatch.design.json +++ b/config/nam.drsquatch.design.json @@ -364,6 +364,43 @@ } } }, + { + "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" } } } ], diff --git a/src/modules/home/components/vt-feedback/index.tsx b/src/modules/home/components/vt-feedback/index.tsx new file mode 100644 index 0000000..f3731c0 --- /dev/null +++ b/src/modules/home/components/vt-feedback/index.tsx @@ -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 ( +
+ {title &&

{title}

} +
+
+ {items.map((it: any, idx: number) => ( +
+
+
{renderStars(it.rating)}
+ {it.title &&
{it.title}
} + {it.text &&
{it.text}
} + {it.author &&
{it.author}
} +
+
+ ))} +
+ {showControls && ( +
+
+ +
+
+ +
+
+ )} +
+
+ ) +} diff --git a/src/vibentec/component-map.tsx b/src/vibentec/component-map.tsx index 5d1e679..b2c56a8 100644 --- a/src/vibentec/component-map.tsx +++ b/src/vibentec/component-map.tsx @@ -28,6 +28,7 @@ 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 @@ -105,6 +106,7 @@ export const componentMap: Record = { VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts), VtCategoryHighlight: nodesContextRenderer(VtCategoryHighlight), VtBrand: nodesContextRenderer(VtBrand), + VtFeedback: nodesContextRenderer(VtFeedback), } export type ComponentName = keyof typeof componentMap