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 + +
    + +
    + ) +} 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} + + +
    + {cheapestPrice && } +
    + + 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 ? ( + Thumbnail + ) : ( +
    + +
    + ) +} + +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)