Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/app/[countryCode]/(main)/products/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { notFound } from "next/navigation"
import { listProducts } from "@lib/data/products"
import { getRegion, listRegions } from "@lib/data/regions"
import ProductTemplate from "@modules/products/templates"
import { HttpTypes } from "@medusajs/types"

type Props = {
params: Promise<{ countryCode: string; handle: string }>
searchParams: Promise<{ v_id?: string }>
}

export async function generateStaticParams() {
Expand Down Expand Up @@ -50,6 +52,23 @@ export async function generateStaticParams() {
}
}

function getImagesForVariant(
product: HttpTypes.StoreProduct,
selectedVariantId?: string
) {
if (!selectedVariantId || !product.variants) {
return product.images
}

const variant = product.variants!.find((v) => v.id === selectedVariantId)
if (!variant || !variant.images.length) {
return product.images
}

const imageIdsMap = new Map(variant.images.map((i) => [i.id, true]))
return product.images!.filter((i) => imageIdsMap.has(i.id))
}

export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params
const { handle } = params
Expand Down Expand Up @@ -82,6 +101,9 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
export default async function ProductPage(props: Props) {
const params = await props.params
const region = await getRegion(params.countryCode)
const searchParams = await props.searchParams

const selectedVariantId = searchParams.v_id

if (!region) {
notFound()
Expand All @@ -92,6 +114,8 @@ export default async function ProductPage(props: Props) {
queryParams: { handle: params.handle },
}).then(({ response }) => response.products[0])

const images = getImagesForVariant(pricedProduct, selectedVariantId)

if (!pricedProduct) {
notFound()
}
Expand All @@ -101,6 +125,7 @@ export default async function ProductPage(props: Props) {
product={pricedProduct}
region={region}
countryCode={params.countryCode}
images={images}
/>
)
}
2 changes: 1 addition & 1 deletion src/lib/data/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const listProducts = async ({
offset,
region_id: region?.id,
fields:
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags",
"*variants.calculated_price,+variants.inventory_quantity,*variants.images,+metadata,+tags,",
...queryParams,
},
headers,
Expand Down
38 changes: 38 additions & 0 deletions src/lib/data/variants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use server"

import { sdk } from "@lib/config"
import { HttpTypes } from "@medusajs/types"

import { getAuthHeaders, getCacheOptions } from "./cookies"

export const retrieveVariant = async (
variant_id: string
): Promise<HttpTypes.StoreProductVariant | null> => {
const authHeaders = await getAuthHeaders()

if (!authHeaders) return null

const headers = {
...authHeaders,
}

const next = {
...(await getCacheOptions("variants")),
}

return await sdk.client
.fetch<{ variant: HttpTypes.StoreProductVariant }>(
`/store/product-variants/${variant_id}`,
{
method: "GET",
query: {
fields: "*images",
},
headers,
next,
cache: "force-cache",
}
)
.then(({ variant }) => variant)
.catch(() => null)
}
24 changes: 23 additions & 1 deletion src/modules/products/components/product-actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { Button } from "@medusajs/ui"
import Divider from "@modules/common/components/divider"
import OptionSelect from "@modules/products/components/product-actions/option-select"
import { isEqual } from "lodash"
import { useParams } from "next/navigation"
import { useParams, usePathname, useSearchParams } from "next/navigation"
import { useEffect, useMemo, useRef, useState } from "react"
import ProductPrice from "../product-price"
import MobileActions from "./mobile-actions"
import { useRouter } from "next/navigation"

type ProductActionsProps = {
product: HttpTypes.StoreProduct
Expand All @@ -31,6 +32,10 @@ export default function ProductActions({
product,
disabled,
}: ProductActionsProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()

const [options, setOptions] = useState<Record<string, string | undefined>>({})
const [isAdding, setIsAdding] = useState(false)
const countryCode = useParams().countryCode as string
Expand Down Expand Up @@ -70,6 +75,23 @@ export default function ProductActions({
})
}, [product.variants, options])

useEffect(() => {
const params = new URLSearchParams(searchParams.toString())
const value = isValidVariant ? selectedVariant?.id : null

if (params.get("v_id") === value) {
return
}

if (value) {
params.set("v_id", value)
} else {
params.delete("v_id")
}

router.replace(pathname + "?" + params.toString())
}, [selectedVariant, isValidVariant])

// check if the selected variant is in stock
const inStock = useMemo(() => {
// If we don't manage inventory, we can always add to cart
Expand Down
9 changes: 6 additions & 3 deletions src/modules/products/templates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import RelatedProducts from "@modules/products/components/related-products"
import ProductInfo from "@modules/products/templates/product-info"
import SkeletonRelatedProducts from "@modules/skeletons/templates/skeleton-related-products"
import { notFound } from "next/navigation"
import ProductActionsWrapper from "./product-actions-wrapper"
import { HttpTypes } from "@medusajs/types"

import ProductActionsWrapper from "./product-actions-wrapper"

type ProductTemplateProps = {
product: HttpTypes.StoreProduct
region: HttpTypes.StoreRegion
countryCode: string
images: HttpTypes.StoreProductImage[]
}

const ProductTemplate: React.FC<ProductTemplateProps> = ({
product,
region,
countryCode,
images,
}) => {
if (!product || !product.id) {
return notFound()
Expand All @@ -29,15 +32,15 @@ const ProductTemplate: React.FC<ProductTemplateProps> = ({
return (
<>
<div
className="content-container flex flex-col small:flex-row small:items-start py-6 relative"
className="content-container flex flex-col small:flex-row small:items-start py-6 relative"
data-testid="product-container"
>
<div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-6">
<ProductInfo product={product} />
<ProductTabs product={product} />
</div>
<div className="block w-full relative">
<ImageGallery images={product?.images || []} />
<ImageGallery images={images} />
</div>
<div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-12">
<ProductOnboardingCta />
Expand Down