From 877d95f7e8750355e299f51df2915ce535cba5a2 Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 9 Oct 2025 21:27:13 +0200 Subject: [PATCH 1/5] wip: variant scoped images --- .../(main)/products/[handle]/page.tsx | 10 +++++ src/lib/data/variants.ts | 38 +++++++++++++++++++ .../components/product-actions/index.tsx | 29 +++++++++++++- .../products/context/variant-context/index.ts | 1 + .../variant-context/variant-context.tsx | 11 ++++++ src/modules/products/templates/index.tsx | 16 ++++++-- 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 src/lib/data/variants.ts create mode 100644 src/modules/products/context/variant-context/index.ts create mode 100644 src/modules/products/context/variant-context/variant-context.tsx diff --git a/src/app/[countryCode]/(main)/products/[handle]/page.tsx b/src/app/[countryCode]/(main)/products/[handle]/page.tsx index cc03fafaf..af148bc12 100644 --- a/src/app/[countryCode]/(main)/products/[handle]/page.tsx +++ b/src/app/[countryCode]/(main)/products/[handle]/page.tsx @@ -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 { retrieveVariant } from "@lib/data/variants" type Props = { params: Promise<{ countryCode: string; handle: string }> + searchParams: { v_id?: string } } export async function generateStaticParams() { @@ -82,6 +84,9 @@ export async function generateMetadata(props: Props): Promise { export default async function ProductPage(props: Props) { const params = await props.params const region = await getRegion(params.countryCode) + const selectedVariantId = props.searchParams.v_id + + let variant if (!region) { notFound() @@ -92,6 +97,10 @@ export default async function ProductPage(props: Props) { queryParams: { handle: params.handle }, }).then(({ response }) => response.products[0]) + if (selectedVariantId) { + variant = await retrieveVariant(pricedProduct.id, selectedVariantId) + } + if (!pricedProduct) { notFound() } @@ -101,6 +110,7 @@ export default async function ProductPage(props: Props) { product={pricedProduct} region={region} countryCode={params.countryCode} + images={variant?.images || pricedProduct.images || []} /> ) } diff --git a/src/lib/data/variants.ts b/src/lib/data/variants.ts new file mode 100644 index 000000000..764958062 --- /dev/null +++ b/src/lib/data/variants.ts @@ -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 ( + product_id: string, + variant_id: string +): Promise => { + 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/products/${product_id}/variants/${variant_id}`, + { + method: "GET", + query: { + fields: "*images", + }, + headers, + next, + cache: "force-cache", + } + ) + .then(({ variant }) => variant) + // .catch(() => null) +} diff --git a/src/modules/products/components/product-actions/index.tsx b/src/modules/products/components/product-actions/index.tsx index 52e6d36f0..d94c8ca6e 100644 --- a/src/modules/products/components/product-actions/index.tsx +++ b/src/modules/products/components/product-actions/index.tsx @@ -6,11 +6,13 @@ import { HttpTypes } from "@medusajs/types" import { Button } from "@medusajs/ui" import Divider from "@modules/common/components/divider" import OptionSelect from "@modules/products/components/product-actions/option-select" +import { VariantContext } from "@modules/products/context/variant-context" import { isEqual } from "lodash" -import { useParams } from "next/navigation" -import { useEffect, useMemo, useRef, useState } from "react" +import { useParams, usePathname, useSearchParams } from "next/navigation" +import { useContext, 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 @@ -31,10 +33,16 @@ export default function ProductActions({ product, disabled, }: ProductActionsProps) { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const [options, setOptions] = useState>({}) const [isAdding, setIsAdding] = useState(false) const countryCode = useParams().countryCode as string + // const { setVariant } = useContext(VariantContext) + // If there is only 1 variant, preselect the options useEffect(() => { if (product.variants?.length === 1) { @@ -70,6 +78,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 diff --git a/src/modules/products/context/variant-context/index.ts b/src/modules/products/context/variant-context/index.ts new file mode 100644 index 000000000..25df834c8 --- /dev/null +++ b/src/modules/products/context/variant-context/index.ts @@ -0,0 +1 @@ +export * from "./variant-context" diff --git a/src/modules/products/context/variant-context/variant-context.tsx b/src/modules/products/context/variant-context/variant-context.tsx new file mode 100644 index 000000000..b13c3d5d3 --- /dev/null +++ b/src/modules/products/context/variant-context/variant-context.tsx @@ -0,0 +1,11 @@ +import { createContext } from "react" + +import { HttpTypes } from "@medusajs/types" + +export const VariantContext = createContext<{ + variant: HttpTypes.StoreProductVariant | null + setVariant: (v: HttpTypes.StoreProductVariant | null) => void +}>({ + variant: null, + setVariant: () => {}, +}) diff --git a/src/modules/products/templates/index.tsx b/src/modules/products/templates/index.tsx index 124616a31..aa33b3642 100644 --- a/src/modules/products/templates/index.tsx +++ b/src/modules/products/templates/index.tsx @@ -8,28 +8,37 @@ 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" +import { VariantContext } from "../context/variant-context" + type ProductTemplateProps = { product: HttpTypes.StoreProduct region: HttpTypes.StoreRegion countryCode: string + images: HttpTypes.StoreProductImage[] } const ProductTemplate: React.FC = ({ product, region, countryCode, + images, }) => { + // const [variant, setVariant] = useState( + // null + // ) + if (!product || !product.id) { return notFound() } return ( <> + {/* */}
@@ -37,7 +46,7 @@ const ProductTemplate: React.FC = ({
- +
@@ -62,6 +71,7 @@ const ProductTemplate: React.FC = ({
+ // {/* */} ) } From 84b79162d45fdace45a94a8e4900e3c20082954b Mon Sep 17 00:00:00 2001 From: fPolic Date: Fri, 10 Oct 2025 12:00:10 +0200 Subject: [PATCH 2/5] fix: search params promise and endpoint --- src/app/[countryCode]/(main)/products/[handle]/page.tsx | 8 ++++---- src/lib/data/variants.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/[countryCode]/(main)/products/[handle]/page.tsx b/src/app/[countryCode]/(main)/products/[handle]/page.tsx index af148bc12..66329beff 100644 --- a/src/app/[countryCode]/(main)/products/[handle]/page.tsx +++ b/src/app/[countryCode]/(main)/products/[handle]/page.tsx @@ -7,7 +7,7 @@ import { retrieveVariant } from "@lib/data/variants" type Props = { params: Promise<{ countryCode: string; handle: string }> - searchParams: { v_id?: string } + searchParams: Promise<{ v_id?: string }> } export async function generateStaticParams() { @@ -84,7 +84,7 @@ export async function generateMetadata(props: Props): Promise { export default async function ProductPage(props: Props) { const params = await props.params const region = await getRegion(params.countryCode) - const selectedVariantId = props.searchParams.v_id + const searchParams = await props.searchParams let variant @@ -97,8 +97,8 @@ export default async function ProductPage(props: Props) { queryParams: { handle: params.handle }, }).then(({ response }) => response.products[0]) - if (selectedVariantId) { - variant = await retrieveVariant(pricedProduct.id, selectedVariantId) + if (searchParams.v_id) { + variant = await retrieveVariant(pricedProduct.id, searchParams.v_id) } if (!pricedProduct) { diff --git a/src/lib/data/variants.ts b/src/lib/data/variants.ts index 764958062..aa05617c6 100644 --- a/src/lib/data/variants.ts +++ b/src/lib/data/variants.ts @@ -22,7 +22,7 @@ export const retrieveVariant = async ( return await sdk.client .fetch<{ variant: HttpTypes.StoreProductVariant }>( - `/store/products/${product_id}/variants/${variant_id}`, + `/store/product-variants/${variant_id}`, { method: "GET", query: { @@ -34,5 +34,5 @@ export const retrieveVariant = async ( } ) .then(({ variant }) => variant) - // .catch(() => null) + .catch(() => null) } From 1ab01f1ee8c9aeaf5bda67ecaad77ea9dbb35a17 Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 13 Oct 2025 17:52:08 +0200 Subject: [PATCH 3/5] feat: use product endpoint for filtering images to use a single fetch --- .../(main)/products/[handle]/page.tsx | 25 +++++++++++++++---- src/lib/data/products.ts | 2 +- src/lib/data/variants.ts | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/app/[countryCode]/(main)/products/[handle]/page.tsx b/src/app/[countryCode]/(main)/products/[handle]/page.tsx index 66329beff..df8b45730 100644 --- a/src/app/[countryCode]/(main)/products/[handle]/page.tsx +++ b/src/app/[countryCode]/(main)/products/[handle]/page.tsx @@ -52,6 +52,23 @@ export async function generateStaticParams() { } } +function getImagesForVariant( + product: HttpTypes.StoreProduct, + selectedVariantId?: string +) { + if (!selectedVariantId) { + return product.images + } + + const variant = product.variants.find((v) => v.id === selectedVariantId) + if (!variant) { + return product.inages + } + + 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 { const params = await props.params const { handle } = params @@ -86,7 +103,7 @@ export default async function ProductPage(props: Props) { const region = await getRegion(params.countryCode) const searchParams = await props.searchParams - let variant + const selectedVariantId = searchParams.v_id if (!region) { notFound() @@ -97,9 +114,7 @@ export default async function ProductPage(props: Props) { queryParams: { handle: params.handle }, }).then(({ response }) => response.products[0]) - if (searchParams.v_id) { - variant = await retrieveVariant(pricedProduct.id, searchParams.v_id) - } + const images = getImagesForVariant(pricedProduct, selectedVariantId) if (!pricedProduct) { notFound() @@ -110,7 +125,7 @@ export default async function ProductPage(props: Props) { product={pricedProduct} region={region} countryCode={params.countryCode} - images={variant?.images || pricedProduct.images || []} + images={images} /> ) } diff --git a/src/lib/data/products.ts b/src/lib/data/products.ts index 680a0d992..e33401259 100644 --- a/src/lib/data/products.ts +++ b/src/lib/data/products.ts @@ -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, diff --git a/src/lib/data/variants.ts b/src/lib/data/variants.ts index aa05617c6..9ddf2ca04 100644 --- a/src/lib/data/variants.ts +++ b/src/lib/data/variants.ts @@ -2,10 +2,10 @@ import { sdk } from "@lib/config" import { HttpTypes } from "@medusajs/types" + import { getAuthHeaders, getCacheOptions } from "./cookies" export const retrieveVariant = async ( - product_id: string, variant_id: string ): Promise => { const authHeaders = await getAuthHeaders() From 5fd101b7b7f31713c7ec49ab0ab68fc255ec9fbb Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 13 Oct 2025 17:54:26 +0200 Subject: [PATCH 4/5] refactor: improve image function --- .../[countryCode]/(main)/products/[handle]/page.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/[countryCode]/(main)/products/[handle]/page.tsx b/src/app/[countryCode]/(main)/products/[handle]/page.tsx index df8b45730..1fc857547 100644 --- a/src/app/[countryCode]/(main)/products/[handle]/page.tsx +++ b/src/app/[countryCode]/(main)/products/[handle]/page.tsx @@ -3,7 +3,7 @@ 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 { retrieveVariant } from "@lib/data/variants" +import { HttpTypes } from "@medusajs/types" type Props = { params: Promise<{ countryCode: string; handle: string }> @@ -56,17 +56,17 @@ function getImagesForVariant( product: HttpTypes.StoreProduct, selectedVariantId?: string ) { - if (!selectedVariantId) { + if (!selectedVariantId || !product.variants) { return product.images } - const variant = product.variants.find((v) => v.id === selectedVariantId) - if (!variant) { - return product.inages + 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)) + return product.images!.filter((i) => imageIdsMap.has(i.id)) } export async function generateMetadata(props: Props): Promise { From 2d4fa9d8706fc15406947272669d424f53f7349f Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 13 Oct 2025 17:56:20 +0200 Subject: [PATCH 5/5] fix: remove context stuff --- .../products/components/product-actions/index.tsx | 5 +---- src/modules/products/context/variant-context/index.ts | 1 - .../context/variant-context/variant-context.tsx | 11 ----------- src/modules/products/templates/index.tsx | 7 ------- 4 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 src/modules/products/context/variant-context/index.ts delete mode 100644 src/modules/products/context/variant-context/variant-context.tsx diff --git a/src/modules/products/components/product-actions/index.tsx b/src/modules/products/components/product-actions/index.tsx index d94c8ca6e..c16f71d0a 100644 --- a/src/modules/products/components/product-actions/index.tsx +++ b/src/modules/products/components/product-actions/index.tsx @@ -6,10 +6,9 @@ import { HttpTypes } from "@medusajs/types" import { Button } from "@medusajs/ui" import Divider from "@modules/common/components/divider" import OptionSelect from "@modules/products/components/product-actions/option-select" -import { VariantContext } from "@modules/products/context/variant-context" import { isEqual } from "lodash" import { useParams, usePathname, useSearchParams } from "next/navigation" -import { useContext, useEffect, useMemo, useRef, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import ProductPrice from "../product-price" import MobileActions from "./mobile-actions" import { useRouter } from "next/navigation" @@ -41,8 +40,6 @@ export default function ProductActions({ const [isAdding, setIsAdding] = useState(false) const countryCode = useParams().countryCode as string - // const { setVariant } = useContext(VariantContext) - // If there is only 1 variant, preselect the options useEffect(() => { if (product.variants?.length === 1) { diff --git a/src/modules/products/context/variant-context/index.ts b/src/modules/products/context/variant-context/index.ts deleted file mode 100644 index 25df834c8..000000000 --- a/src/modules/products/context/variant-context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./variant-context" diff --git a/src/modules/products/context/variant-context/variant-context.tsx b/src/modules/products/context/variant-context/variant-context.tsx deleted file mode 100644 index b13c3d5d3..000000000 --- a/src/modules/products/context/variant-context/variant-context.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createContext } from "react" - -import { HttpTypes } from "@medusajs/types" - -export const VariantContext = createContext<{ - variant: HttpTypes.StoreProductVariant | null - setVariant: (v: HttpTypes.StoreProductVariant | null) => void -}>({ - variant: null, - setVariant: () => {}, -}) diff --git a/src/modules/products/templates/index.tsx b/src/modules/products/templates/index.tsx index aa33b3642..dd07163fd 100644 --- a/src/modules/products/templates/index.tsx +++ b/src/modules/products/templates/index.tsx @@ -11,7 +11,6 @@ import { notFound } from "next/navigation" import { HttpTypes } from "@medusajs/types" import ProductActionsWrapper from "./product-actions-wrapper" -import { VariantContext } from "../context/variant-context" type ProductTemplateProps = { product: HttpTypes.StoreProduct @@ -26,17 +25,12 @@ const ProductTemplate: React.FC = ({ countryCode, images, }) => { - // const [variant, setVariant] = useState( - // null - // ) - if (!product || !product.id) { return notFound() } return ( <> - {/* */}
= ({
- // {/*
*/} ) }