diff --git a/config/nam.vibentec.design.json b/config/nam.vibentec.design.json
index f5b610a..1f57e5c 100644
--- a/config/nam.vibentec.design.json
+++ b/config/nam.vibentec.design.json
@@ -211,6 +211,41 @@
}
}
},
+ {
+ "VtFeaturedProducts": {
+ "config": {
+ "title": "best-seller",
+ "styles": {
+ "container": "content-container py-12 px-[100px] small:py-24",
+ "header": {
+ "container": "flex justify-between mb-8",
+ "title": "txt-xlarge",
+ "link": ""
+ },
+ "list": "grid grid-cols-2 small:grid-cols-3 gap-x-6 gap-y-24 small:gap-y-36",
+ "productCard": {
+ "className": "relative overflow-hidden rounded-2xl border border-[#285A86] bg-ui-bg-base shadow-elevation-card-rest h-full flex flex-col",
+ "badge": {
+ "container": "p-4",
+ "text": "z-20 px-3 py-1 border-[0.5px] rounded bg-[#c9e0f5] txt-compact-small-plus shadow-borders-base text-[#285A86]"
+ },
+ "thumbnail": {
+ "className": "rounded-none h-[240px]",
+ "size": "full"
+ },
+ "content": "flex flex-col flex-1 justify-between p-4",
+ "title": "text-ui-fg-subtle text-[18px]",
+ "price": "flex items-center gap-x-1 text-[#285A86] font-bold",
+ "button": {
+ "addToCart": "w-full h-[40px] bg-black text-white rounded-md",
+ "moreInfo": "w-full h-[40px] border border-[#285A86] text-[#285A86] rounded-md"
+ }
+ }
+ }
+ }
+ }
+ },
+
{
"CartMismatchBanner": {
"config": {
diff --git a/src/app/[countryCode]/(main)/layout.tsx b/src/app/[countryCode]/(main)/layout.tsx
index 6036614..bb04592 100644
--- a/src/app/[countryCode]/(main)/layout.tsx
+++ b/src/app/[countryCode]/(main)/layout.tsx
@@ -8,11 +8,19 @@ import { DynamicLayoutRenderer } from "../../../vibentec/renderer"
import { LayoutContext, LayoutComponentNode, } from "../../../vibentec/component-map"
import { loadDesignConfig } from "vibentec/configloader"
+import { getRegion } from "@lib/data/regions"
+
export const metadata: Metadata = {
metadataBase: new URL(getBaseURL()),
}
-export default async function PageLayout(props: { children: React.ReactNode }) {
+export default async function PageLayout(props: {
+ children: React.ReactNode
+ params: Promise<{ countryCode: string }>
+}) {
+ const params = await props.params
+ const { countryCode } = params
+ const region = await getRegion(countryCode)
const customer = await retrieveCustomer()
const cart = await retrieveCart()
let shippingOptions: StoreCartShippingOption[] = []
@@ -29,6 +37,8 @@ export default async function PageLayout(props: { children: React.ReactNode }) {
cart,
shippingOptions,
contentChildren: props.children,
+ countryCode,
+ region,
}
diff --git a/src/app/[countryCode]/(main)/page.tsx b/src/app/[countryCode]/(main)/page.tsx
index 7598b51..ea436af 100644
--- a/src/app/[countryCode]/(main)/page.tsx
+++ b/src/app/[countryCode]/(main)/page.tsx
@@ -4,6 +4,7 @@ import FeaturedProducts from "@modules/home/components/featured-products"
import Hero from "@modules/home/components/hero"
import { listCollections } from "@lib/data/collections"
import { getRegion } from "@lib/data/regions"
+import VtFeaturedProducts from "@modules/home/components/vt-featured-products"
export const metadata: Metadata = {
title: "Medusa Next.js Starter Template",
@@ -23,22 +24,20 @@ export default async function Home(props: {
const { collections } = await listCollections({
fields: "id, handle, title",
})
- const res = await listCollections({
- fields: "id, handle, title",
- })
+
+ console.log('collections:',collections)
if (!collections || !region) {
return null
}
- console.log(res, '--------------')
return (
<>
{/* */}
-
*/}
>
)
}
diff --git a/src/lib/data/collections.ts b/src/lib/data/collections.ts
index cb403ee..134462f 100644
--- a/src/lib/data/collections.ts
+++ b/src/lib/data/collections.ts
@@ -36,7 +36,6 @@ export const listCollections = async (
{
query: queryParams,
next,
- cache: "force-cache",
}
)
.then(({ collections }) => ({ collections, count: collections.length }))
diff --git a/src/lib/data/products.ts b/src/lib/data/products.ts
index 680a0d9..97b8a85 100644
--- a/src/lib/data/products.ts
+++ b/src/lib/data/products.ts
@@ -68,7 +68,6 @@ export const listProducts = async ({
},
headers,
next,
- cache: "force-cache",
}
)
.then(({ products, count }) => {
diff --git a/src/modules/home/components/vt-featured-products/index.tsx b/src/modules/home/components/vt-featured-products/index.tsx
new file mode 100644
index 0000000..169c41e
--- /dev/null
+++ b/src/modules/home/components/vt-featured-products/index.tsx
@@ -0,0 +1,52 @@
+import { HttpTypes } from "@medusajs/types"
+import ProductRail from "./product-rail"
+import { listCollections } from "@lib/data/collections"
+import { LayoutComponentDefinition, LayoutContext } from "@vibentec/component-map"
+
+export default async function VtFeaturedProducts(props: {
+ collections?: HttpTypes.StoreCollection[]
+ region?: HttpTypes.StoreRegion
+ countryCode?: string
+ nodes?: LayoutComponentDefinition
+ context?: LayoutContext
+}) {
+ let { collections, region, countryCode } = props
+ const { nodes, context } = props
+
+ if (context) {
+ if (!region) region = context.region
+ if (!countryCode) countryCode = context.countryCode
+ }
+
+ if (!collections && region) {
+ const result = await listCollections({
+ fields: "id, handle, title",
+ })
+ collections = result.collections
+ }
+
+ if (!collections || !region || !countryCode) {
+ return null
+ }
+
+ const configTitle = nodes?.config?.title
+ const styles = nodes?.config?.styles
+
+ let displayCollections = collections
+ if (configTitle) {
+ displayCollections = collections.filter(
+ (c) => c.handle === configTitle || c.title === configTitle
+ )
+ }
+
+ return displayCollections.map((collection) => (
+
+
+
+ ))
+}
diff --git a/src/modules/home/components/vt-featured-products/product-rail/index.tsx b/src/modules/home/components/vt-featured-products/product-rail/index.tsx
new file mode 100644
index 0000000..ed731b6
--- /dev/null
+++ b/src/modules/home/components/vt-featured-products/product-rail/index.tsx
@@ -0,0 +1,64 @@
+import { listProducts } from "@lib/data/products"
+import { HttpTypes } from "@medusajs/types"
+import { Text, clx } from "@medusajs/ui"
+
+import InteractiveLink from "@modules/common/components/interactive-link"
+import ProductCard from "@modules/products/components/vt-product-card"
+
+export default async function ProductRail({
+ collection,
+ region,
+ countryCode,
+ styles,
+}: {
+ collection: HttpTypes.StoreCollection
+ region: HttpTypes.StoreRegion
+ countryCode: string
+ styles?: any
+}) {
+ const {
+ response: { products: pricedProducts },
+ } = await listProducts({
+ regionId: region.id,
+ queryParams: {
+ collection_id: collection.id,
+ fields: "*variants.calculated_price",
+ },
+ })
+
+ if (!pricedProducts) {
+ return null
+ }
+
+ const classes = {
+ container: styles?.container || "content-container py-12 px-[100px] small:py-24",
+ header: {
+ container: styles?.header?.container || "flex justify-between mb-8",
+ title: styles?.header?.title || "txt-xlarge",
+ },
+ list: styles?.list || "grid grid-cols-2 small:grid-cols-3 gap-x-6 gap-y-24 small:gap-y-36",
+ }
+
+ return (
+
+
+ {collection.title}
+
+ View all
+
+
+
+ {pricedProducts &&
+ pricedProducts.map((product) => (
+ -
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/modules/products/components/vt-product-card/index.tsx b/src/modules/products/components/vt-product-card/index.tsx
new file mode 100644
index 0000000..08a4270
--- /dev/null
+++ b/src/modules/products/components/vt-product-card/index.tsx
@@ -0,0 +1,156 @@
+import { HttpTypes } from "@medusajs/types"
+import { Button, Heading, Text, clx } from "@medusajs/ui"
+import LocalizedClientLink from "@modules/common/components/localized-client-link"
+import Divider from "@modules/common/components/divider"
+import PreviewPrice from "@modules/products/components/product-preview/price"
+import { getProductPrice } from "@lib/util/get-product-price"
+import { addToCart } from "@lib/data/cart"
+import VtThumbnail from "../vt-thumbnail"
+
+type ProductCardProps = {
+ product: HttpTypes.StoreProduct
+ badgeText?: string
+ deliveryTime?: string
+ className?: string
+ countryCode: string
+ styles?: any
+}
+
+export default function ProductCard({
+ product,
+ badgeText = "Saved up to 20%",
+ deliveryTime = "2-4 Wochen",
+ className,
+ countryCode,
+ styles,
+}: ProductCardProps) {
+ const firstVariant = product.variants?.[0]
+
+ const inStock = (() => {
+ if (!firstVariant) return false
+ if (!firstVariant.manage_inventory) return true
+ if (firstVariant.allow_backorder) return true
+ return (firstVariant.inventory_quantity || 0) > 0
+ })()
+
+ const { cheapestPrice } = getProductPrice({ product })
+
+ async function handleAddToCart() {
+ "use server"
+ if (!firstVariant?.id) return
+ await addToCart({
+ variantId: firstVariant.id,
+ quantity: 1,
+ countryCode,
+ })
+ }
+
+ const description = (() => {
+ const description = product.description || ""
+ const textSlice = description.length > 120 ? description.slice(0, 117) + "…" : description
+ return textSlice
+ })()
+
+ const classes = {
+ card: styles?.className || className || "relative overflow-hidden rounded-2xl border border-[#285A86] bg-ui-bg-base shadow-elevation-card-rest h-full flex flex-col",
+ badge: {
+ container: styles?.badge?.container || "p-4",
+ text: styles?.badge?.text || "z-20 px-3 py-1 border-[0.5px] rounded bg-[#c9e0f5] txt-compact-small-plus shadow-borders-base text-[#285A86] ",
+ },
+ thumbnail: {
+ className: styles?.thumbnail?.className || "rounded-none h-[240px]",
+ size: styles?.thumbnail?.size || "full",
+ },
+ content: styles?.content || "p-6 flex flex-col flex-1",
+ title: styles?.title || "mt-2 text-ui-fg-base",
+ price: styles?.price || "mt-2 flex items-baseline gap-2",
+ button: {
+ addToCart: styles?.button?.addToCart || "flex-1",
+ moreInfo: styles?.button?.moreInfo || "w-full",
+ },
+ }
+
+ return (
+
+
+ {badgeText && (
+
+
+ {badgeText}
+
+
+ )}
+
+
+
+
+ {product.collection && (
+
+ {product.collection.title}
+
+ )}
+
+
+ {product.title}
+
+
+
+
+ inkl. MwSt. zzgl. Versandkosten
+
+
+
+
+
{description}
+
+ Lieferzeit: {deliveryTime}
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/modules/products/components/vt-thumbnail/index.tsx b/src/modules/products/components/vt-thumbnail/index.tsx
new file mode 100644
index 0000000..728bef3
--- /dev/null
+++ b/src/modules/products/components/vt-thumbnail/index.tsx
@@ -0,0 +1,70 @@
+import { Container, clx } from "@medusajs/ui"
+import Image from "next/image"
+import React from "react"
+
+import PlaceholderImage from "@modules/common/icons/placeholder-image"
+
+type ThumbnailProps = {
+ thumbnail?: string | null
+ // TODO: Fix image typings
+ images?: any[] | null
+ size?: "small" | "medium" | "large" | "full" | "square"
+ isFeatured?: boolean
+ className?: string
+ "data-testid"?: string
+}
+
+const VtThumbnail: React.FC = ({
+ thumbnail,
+ images,
+ size = "small",
+ isFeatured,
+ className,
+ "data-testid": dataTestid,
+}) => {
+ const initialImage = thumbnail || images?.[0]?.url
+
+ return (
+
+
+
+ )
+}
+
+const ImageOrPlaceholder = ({
+ image,
+ size,
+}: Pick & { image?: string }) => {
+ return image ? (
+
+ ) : (
+
+ )
+}
+
+export default VtThumbnail
diff --git a/src/vibentec/component-map.tsx b/src/vibentec/component-map.tsx
index b1c1dcd..9b196dc 100644
--- a/src/vibentec/component-map.tsx
+++ b/src/vibentec/component-map.tsx
@@ -25,6 +25,7 @@ import VtFooterSignUp from "@modules/layout/templates/vt-footer/vt-footer-signup
import Hero from "@modules/layout/templates/hero"
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"
type ComponentConfig = Record
@@ -38,6 +39,8 @@ export interface LayoutContext {
cart: any
shippingOptions: any[]
contentChildren: React.ReactNode
+ countryCode?: string
+ region?: any
}
export type ComponentRenderer = {
@@ -97,6 +100,7 @@ export const componentMap: Record = {
VtFooterSignUp: nodesContextRenderer(VtFooterSignUp),
Footer: nodesContextRenderer(VtFooter),
ImageDisplayer: nodesContextRenderer(VtCarousel),
+ VtFeaturedProducts: nodesContextRenderer(VtFeaturedProducts),
}
export type ComponentName = keyof typeof componentMap
diff --git a/src/vibentec/configloader.ts b/src/vibentec/configloader.ts
index dd5fbef..ad9c8e0 100644
--- a/src/vibentec/configloader.ts
+++ b/src/vibentec/configloader.ts
@@ -2,7 +2,7 @@ import fs from "fs"
import path from "path"
import { jsonFileNames } from "./devJsonFileNames";
-const fileName = jsonFileNames.namStarter;
+const fileName = jsonFileNames.namVibentec;
export async function loadDesignConfig() {
const filePath = path.join(process.cwd(), "config", fileName)