Modificar pƔgina de producto
Introducción
La pĆ”gina de producto es una de las pĆ”ginas que hay que modificar en casi todos los proyectos. Normalmente no son suficientes los cambios que se pueden llegar a hacer mediante ajuste de estilos, componentes o configuraciones y es entonces cuando hay que sobreescribir la pĆ”gina para tener libertad total para modificar el layout, aƱadir mĆ”s información, etcā¦
Las pƔginas de los proyectos Mercury, por defecto, tienen todas una misma estructura. Importan la parte cliente, que se encarga de renderizar la pƔgina, y la parte servidor, que se encarga de preparar los datos para la pƔgina.
SegĆŗn lo que queramos hacer deberemos sobreescribir la parte cliente, la parte servidor o ambas.
Mostrar descripción del producto debajo del nombre
1. Sobreescribir la pƔgina de producto
La pƔgina debemos sobrreescribir para modificar la pƔgina de producto es:
import { Page } from '@mercury/theme/pages/product/client'
export { getStaticProps, getStaticPaths } from '@mercury/theme/pages/product/server'
export default function Product (props) {
return <Page {...props} />
}
Nuestro cambio solo afecta a la parte cliente ya que solo se trata de mover una información que ya se muestra a otro sitio. En este caso solo serÔ necesario quitar el import del cliente y copiar el contenido de la pÔgina original de Mercury:
- node_modules/@mercury/theme/src/pages/product/client.tsx
El resultado:
export { getStaticProps, getStaticPaths } from '@mercury/theme/pages/product/server'
import { setApolloStoreCode, getProductUrlKeys, getStoreConfig } from '@mercury/service-adobe-commerce'
import { prepareProductPageData, serviceMiddleware } from '@mercury/service-adobe-commerce/ssr'
import { useContext, useEffect, useRef, useState } from 'react'
import Head from 'next/head'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Accordion, AmountSelector, Breadcrumb, HomeIcon } from '@mercury/ui'
import { Price, Gallery, AddToCart, ProductCarousel, ProductVariants, ProductBundleOptions, ProductTitle, AddToWishlist, AdditionalInformation } from '@mercury/theme/components'
import { ProductPricesContext, useProduct, useProductPrice } from '@mercury/service-adobe-commerce'
import { useI18n } from '@mercury/i18n'
import type { ProductType } from '@mercury/models'
export function Page ({ storeConfig, productData, productAdditionalInformation }) {
const { t } = useI18n()
const router = useRouter()
const { query } = router
const { urlKey, added = false } = query
const { product: data, loading, error } = useProduct({ urlKey })
const [product, setProduct] = useState<ProductType>(null)
const { loadPrices } = useContext(ProductPricesContext)
const { price, loading: loadingPrice } = useProductPrice({ sku: product?.sku })
const [selectedOptionsUids, setSelectedOptionsUids] = useState<Array<string>>(null)
const addToCartRef = useRef<HTMLButtonElement>(null)
const { defaultTitle, defaultKeywords, defaultRobots } = storeConfig
const { name: productName, metaTitle, metaDescription, metaKeywords } = productData as ProductType
useEffect(() => {
if (data) {
setProduct(data)
}
}, [data])
useEffect(() => {
if (!product?.sku) return
loadPrices([product.sku])
}, [product, loadPrices])
const handleVariantChange = (selectedOptions, selectedVariantProduct) => {
if (selectedVariantProduct) {
const updateAttributes = {
sku: selectedOptions.length === productData.variants.length ? selectedVariantProduct.sku : productData.sku,
name: selectedVariantProduct.name,
mediaGallery: selectedVariantProduct.mediaGallery.length > 0 ? selectedVariantProduct.mediaGallery : productData.mediaGallery,
price: selectedVariantProduct.price
}
setProduct({ ...product, ...updateAttributes })
}
}
const handleBundleOptionChange = (selectedOptions) => {
if (selectedOptions && selectedOptions.length > 0) {
const priceIncrement = selectedOptions.reduce((prev, curr) => (prev?.item ? prev.item.priceIncrement : prev) + curr?.item.priceIncrement, 0)
const updateAttributes = {
price: {
regularPrice: productData.price.regularPrice,
finalPrice: productData.price.finalPrice + priceIncrement
}
}
const selectedOptionsUids = selectedOptions.map(option => option.item.uid)
setSelectedOptionsUids(selectedOptionsUids)
setProduct({ ...product, ...updateAttributes })
}
}
const [quantity, setQuantity] = useState<number | string>(1)
useEffect(() => {
if (!added || !product?.sku) return
// remove added query
delete router.query.added
router.replace({
pathname: router.pathname,
query: router.query
}, undefined, { shallow: true })
addToCartRef.current.click()
}, [added, product, router])
if (loading || error || !product) return null
return (<>
<Head>
<title>{metaTitle || productName || defaultTitle}</title>
{ metaDescription && <meta name="description" content={metaDescription} /> }
<meta name="keywords" content={metaKeywords || defaultKeywords} />
<meta name="robots" content={defaultRobots} />
<link rel="icon" href="/favicon.ico" />
</Head>
<section className='pt-8 pb-10'>
<Breadcrumb>
<Link href="/"> <HomeIcon className="w-4 h-4" variant="light" /> </Link>
{product.categories.map(({ uid, urlPath, name }) =>
<Link key={uid} href={`/${urlPath}`}> {name} </Link>
)}
</Breadcrumb>
<div className='flex flex-col gap-8 lg:flex-row'>
<div className='block md:hidden'>
<ProductTitle name={product.name} price={price ?? product.price} loading={loadingPrice} />
</div>
<Gallery enableMaximized={true} images={product.mediaGallery} />
<div className='min-w-[40%]'>
<div className='p-8 bg-white'>
<div className='hidden md:block'>
<ProductTitle name={product.name} price={price ?? product.price} loading={loadingPrice} />
</div>
{/* Bundle Options */}
{product.bundleOptions?.length > 0 &&
<div className='md:w-full md:max-w-[420px] mt-6 py-2 px-0 border border-white'>
<ProductBundleOptions options={product.bundleOptions} onChange={handleBundleOptionChange}/>
</div>
}
<div className='mb-6'>
<Price price={price?.finalPrice} regularPrice={price?.regularPrice} loading={loadingPrice} />
</div>
{/* Variants */}
{product.variants?.length > 0 &&
<div className='md:w-full md:max-w-[330px] mt-6 py-2 px-0 border border-white'>
<ProductVariants options={product.variants} onChange={handleVariantChange}/>
</div>
}
{/* Product Actions */}
<div className='flex flex-col gap-2 my-6 md:flex-row md:items-center'>
<AmountSelector className='max-w-[120px] md:w-full md:max-w-[165px] h-[40px]' amount={quantity} setAmount={setQuantity}/>
<AddToCart buttonSize='md' borderRadius='lg' className='h-[40px]' ref={addToCartRef} productToAdd={product} parentSku={productData.sku} selectedOptions={selectedOptionsUids} quantity={Number(quantity)}>
{t('cart.add')}
</AddToCart>
</div>
<div className='pt-4 border-t border-neutral-200'>
<AddToWishlist product={product}/>
</div>
</div>
</div>
</div>
<div className='flex flex-col px-4 mt-6 bg-white'>
{/* Product description */}
<Accordion title={t('productPage.productInfo')} defaultOpen={true} className='px-2 py-2 text-xl font-bold leading-5 text-left border-b h-14 hover:text-neutral-700 focus:outline-none'>
<div className='px-3 py-6 text-md text-neutral-800'>
<p className='text-md text-neutral-800 product-description' dangerouslySetInnerHTML={{ __html: product.description }}></p>
</div>
</Accordion>
{/* Additional information */}
<Accordion title={t('productPage.productExtraInfo')} className='px-2 py-2 text-xl font-bold leading-5 text-left border-b h-14 hover:text-neutral-700 focus:outline-none'>
<div className='px-3 py-6 text-md text-neutral-800'>
<AdditionalInformation additionalInformation={ productAdditionalInformation} />
</div>
</Accordion>
</div>
{/* Related products */}
{
product.relatedProducts.length > 0 &&
<div className='mt-16'>
<p className='px-2 pt-2 pb-4 mb-6 text-xl font-bold leading-5 text-left uppercase border-b'>
{t('productPage.relatedProducts')}
</p>
<ProductCarousel sku={product.relatedProducts} />
</div>
}
{/* Upsell products */}
{
product.upsellProducts.length > 0 &&
<div className='mt-16'>
<p className='px-2 pt-2 pb-4 mb-6 text-xl font-bold leading-5 text-left uppercase border-b'>
{t('productPage.upsellProducts')}
</p>
<ProductCarousel sku={product.upsellProducts} />
</div>
}
</section>
</>)
}
2. Mostrar descripción debajo del nombre
Ahora moveremos la descripción del lugar donde se muestra originalmente par que se muestre debajo del nombre del producto.
return (<>
<Head>
<title>{metaTitle || productName || defaultTitle}</title>
{ metaDescription && <meta name="description" content={metaDescription} /> }
<meta name="keywords" content={metaKeywords || defaultKeywords} />
<meta name="robots" content={defaultRobots} />
<link rel="icon" href="/favicon.ico" />
</Head>
<section className='pt-8 pb-10'>
<Breadcrumb>
<Link href="/"> <HomeIcon className="w-4 h-4" variant="light" /> </Link>
{product.categories.map(({ uid, urlPath, name }) =>
<Link key={uid} href={`/${urlPath}`}> {name} </Link>
)}
</Breadcrumb>
<div className='flex flex-col gap-8 lg:flex-row'>
<div className='block md:hidden'>
<ProductTitle name={product.name} price={price ?? product.price} loading={loadingPrice} />
+
+ {/* Product description */}
+ <div className='px-3 py-6 text-md text-neutral-800'>
+ <p className='text-md text-neutral-800 product-description' dangerouslySetInnerHTML={{ __html: product.description }}></p>
+ </div>
+
</div>
<Gallery enableMaximized={true} images={product.mediaGallery} />
<div className='min-w-[40%]'>
<div className='p-8 bg-white'>
<div className='hidden md:block'>
<ProductTitle name={product.name} price={price ?? product.price} loading={loadingPrice} />
</div>
+
+ {/* Product description */}
+ <div className='px-3 py-6 text-md text-neutral-800'>
+ <p className='text-md text-neutral-800 product-description' dangerouslySetInnerHTML={{ __html: product.description }}></p>
+ </div>
+
{/* Bundle Options */}
{product.bundleOptions?.length > 0 &&
<div className='md:w-full md:max-w-[420px] mt-6 py-2 px-0 border border-white'>
<ProductBundleOptions options={product.bundleOptions} onChange={handleBundleOptionChange}/>
</div>
}
<div className='mb-6'>
<Price price={price?.finalPrice} regularPrice={price?.regularPrice} loading={loadingPrice} />
</div>
{/* Variants */}
{product.variants?.length > 0 &&
<div className='md:w-full md:max-w-[330px] mt-6 py-2 px-0 border border-white'>
<ProductVariants options={product.variants} onChange={handleVariantChange}/>
</div>
}
{/* Product Actions */}
<div className='flex flex-col gap-2 my-6 md:flex-row md:items-center'>
<AmountSelector className='max-w-[120px] md:w-full md:max-w-[165px] h-[40px]' amount={quantity} setAmount={setQuantity}/>
<AddToCart buttonSize='md' borderRadius='lg' className='h-[40px]' ref={addToCartRef} productToAdd={product} parentSku={productData.sku} selectedOptions={selectedOptionsUids} quantity={Number(quantity)}>
{t('cart.add')}
</AddToCart>
</div>
<div className='pt-4 border-t border-neutral-200'>
<AddToWishlist product={product}/>
</div>
</div>
</div>
</div>
<div className='flex flex-col px-4 mt-6 bg-white'>
- {/* Product description */}
- <Accordion title={t('productPage.productInfo')} defaultOpen={true} className='px-2 py-2 text-xl font-bold leading-5 text-left border-b h-14 hover:text-neutral-700 focus:outline-none'>
- <div className='px-3 py-6 text-md text-neutral-800'>
- <p className='text-md text-neutral-800 product-description' dangerouslySetInnerHTML={{ __html: product.description }}></p>
- </div>
- </Accordion>
-
{/* Additional information */}
<Accordion title={t('productPage.productExtraInfo')} className='px-2 py-2 text-xl font-bold leading-5 text-left border-b h-14 hover:text-neutral-700 focus:outline-none'>
<div className='px-3 py-6 text-md text-neutral-800'>
<AdditionalInformation additionalInformation={ productAdditionalInformation} />
</div>
</Accordion>
</div>
{/* Related products */}
{
product.relatedProducts.length > 0 &&
<div className='mt-16'>
<p className='px-2 pt-2 pb-4 mb-6 text-xl font-bold leading-5 text-left uppercase border-b'>
{t('productPage.relatedProducts')}
</p>
<ProductCarousel sku={product.relatedProducts} />
</div>
}
{/* Upsell products */}
{
product.upsellProducts.length > 0 &&
<div className='mt-16'>
<p className='px-2 pt-2 pb-4 mb-6 text-xl font-bold leading-5 text-left uppercase border-b'>
{t('productPage.upsellProducts')}
</p>
<ProductCarousel sku={product.upsellProducts} />
</div>
}
</section>
</>)
}