Desarrollar con Mercury

Introducción

Entendemos como desarrollar cuando vamos a trabajar en una aplicación (site) que usa Mercury como frontend.

Requerimientos

  • NodeJS 16
  • Google Artifact Registry
  • YARN

Instalación de requerimientos

NodeJS

nvm para poder instalar y cambiar fácilmente entre versiones de NodeJS.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
nvm install 16

Google Artifact Registry

Tener NPM configurado para poder descargar paquetes de repositorio privado de google e instalar gestor de dependencias para Javascript.

npx google-artifactregistry-auth
npm install -g npm@9.4.0

YARN

YARN (alternativa a NPM, la cual es la usada en el proyecto)

npm install -g yarn

Preparación de entorno local

1. Clonar proyecto

Tras clonar el repositorio del proyecto donde se esté desarrollando el frontal del site con Mercury, por ejemplo:

git clone git@bitbucket.org:etailers/eshop-horeca-front.git

La estructura de directorios sería como la siguiente:

Estructura directorios del proyecto.

Trabajaremos en el directorio mercury que es donde se encuentran los ficheros de nuestra aplicación. Dentro de este directorio encontramos:

public
Contiene los ficheros de acceso público: imágenes, tipografías, etc…
src
Contiene los archivos de nuestra aplicación: páginas, componentes, servicios, etc…

Los detalles de esta estructura de directorios los iremos viendo en la documentación.

2. Instalar paquetes

Una vez tengamos descargada la aplicación iremos al root del proyecto e instalaremos todos los paquetes:

yarn preinstall
yarn

3. Configurar Mercury

Copiaremos la configuración local de ejemplo y haremos los ajustes necesaris para que funcione con nuestro entorno.

cp mercury/.env.local.dist mercury/.env.local

Para trabajar contra una aplicación que tengamos en local:

NODE_TLS_REJECT_UNAUTHORIZED=0
NEXT_DEV_PUBLIC_API_ENDPOINT=http://localhost:3000/api/
NEXT_PUBLIC_IMAGE_DOMAIN=magento.test
NEXT_PUBLIC_API_ENDPOINT=https://magento.test/graphql
TET_API_SECRET=<token_configurado_en_la_configuracion_del_ecomm>

4 - Iniciar proyecto

yarn dev

5 - Acceder a la aplicación

Tras iniciar el proyecto ya podremos entrar la url en nuestro navegador.

https://magento.test

Comandos

Podemos consultar los comandos disponibles en el fichero package.json del root del proyecto:

yarn dev
Inicializa la aplicación en modo desarrollo.
yarn build
Lo usaremos cuando queramos hacer “build” de la aplicación. Es recomendable ejecutarlo antes de hacer un “merge” de nuestras modificaciones para comprobar que no hay ningún error, y para testear la aplicación como si estuviéramos en modo producción.
yarn start

Inicializa la aplicación que hemos construido. Se usa junto con “build” para testear la aplicación como si estuviéramos en modo producción.

yarn lint

Ejecuta el linter para poder encontrar errores en la aplicación.

yarn clear

Para limpiar la instalación. Después de este comando habrá que ejecutar “yarn” para que vuelva a instalar todos los paquetes.

yarn preinstall

Ejecuta npx google-artifactregistry-auth para permitir la descarga de paquetes de glcoud.

yarn install

Para descargar e instalar todos los paquetes.

yarn upgrade-packages

Actualiza todos los paquetes de la aplicación.

Configuración

Dentro del directorio de mercury encontramos el fichero mercury.config.js. Este fichero nos permite configurar comportamientos globales del tema.

Tiene 2 apartados principales:

components

Nos permite configurar propiedades generales de los componentes de la aplicación. Hay muchos componentes que nos permitirán configurar variaciones desde este archivo de configuración. Por ejemplo: logo, menu, quickSearch, etc…

tailwind

En este apartado podemos configurar los estilos de las clases generales de tailwind de la misma forma que lo haríamos en tailwind.config.js.

Customización

Ahora que ya tenemos la aplicación instalada en local y los conocimientos básicos de la estructura de directorios de Mercury ya estamos listos para ver como podemos empezar a realizar modificaciones a la aplicación.

Estilos

Mercury usa Tailwind para los estilos CSS de la aplicación.

Existen 2 sitios donde podemos ajustar los estilos de la aplicación dependiendo de nuestro propósito:

mercury.config.js

En este archivo podemos ajustar los estilos generales de las clases de tailwind. Como hemos visto en el apartado de “Configuración”.

styles/theme.css

En este fichero podemos sobreescribir las clases que no son de tailwind y que usan los componentes de Mercury.

A continuación dejamos un ejemplo del tipo de clases que podemos sobreescribir en styles/themes.css

styles/themes.css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
    @font-face {
        font-family: 'Switzer';
        src: url('/fonts/Switzer-Light.woff2') format('woff2');
        font-weight: 300;
        font-display: swap;
        font-style: normal;
    }

    @font-face {
        font-family: 'Switzer';
        src: url('/fonts/Switzer-Regular.woff2') format('woff2');
        font-weight: 400;
        font-display: swap;
        font-style: normal;
    }

    @font-face {
        font-family: 'Switzer';
        src: url('/fonts/Switzer-Medium.woff2') format('woff2');
        font-weight: 500;
        font-display: swap;
        font-style: normal;
    }

    @font-face {
        font-family: 'Switzer';
        src: url('/fonts/Switzer-Semibold.woff2') format('woff2');
        font-weight: 600;
        font-display: swap;
        font-style: normal;
    }

    body {
        @apply font-sans text-base text-gray-900 bg-[#FBFDFA];
    }

    h1 {
        @apply text-[36px] leading-[44px] lg:text-[40px] lg:leading-[48px]
    }

    h2 {
        @apply text-[32px] leading-[40px] lg:text-[36px] lg:leading-[44px]
    }

    h3 {
        @apply text-[28px] leading-[36px] lg:text-[32px] lg:leading-[40px]
    }
    
    ...
    
    .footer-cms-content {
        @apply !m-0 w-full lg:justify-end justify-center max-lg:pt-8
    }
    .footer-cms-content a {
        @apply block text-base py-2
    }
    footer [sanitized-element="column-group"] {
        @apply sm:!gap-16 gap-8 md:flex-nowrap flex-wrap basis-full lg:justify-end justify-center
    }
    footer [sanitized-element="row"] {
        @apply w-full
    }
    footer [sanitized-element="column"] {
        /*min-width: 240px;*/
        @apply sm:!w-min sm:basis-0 basis-full whitespace-nowrap md:text-left text-center
    }
}

Páginas

Mercury usa el sistema de enrutado de páginas de NextJS (ver documentación). Las páginas de la aplicación se encuentran en el directorio pages de mercury/src.

ls mercury/src/pages

Por defecto existen las siguientes páginas:

Home
index.tsx
Categorías
[…urlKey].tsx
Producto
product/[…urlKey].tsx
Búsqueda
search.tsx
Página CMS
page/[…urlKey].tsx
Mi cuenta
account/[[…section]].tsx
Carrito
cart.tsx
Errores
404.tsx, 500.tsx

Estructura de una página Mercury

Las páginas de la aplicación Mercury, por defecto, tienen todas una misma estructura.

mercury/src/pages/index.tsx
import { Page } from '@mercury/theme/pages/home/client'
export { getStaticProps } from '@mercury/theme/pages/home/server'

export default function Home (props) {
  return <Page {...props} />
}

Esta estructura de página nos permite poder sobreescribir el renderizado de la página sin tener que tocar la obtención de propiedades.

Siguiendo el ejemplo anterior, si queremos para sobreescribir la página mercury/src/pages/index.tsx.

mercury/src/pages/index.tsx
import Head from 'next/head'
import { CmsContent } from '@mercury/theme/components'
import type { CmsPageType } from '@mercury/models'
export { getStaticProps } from '@mercury/theme/pages/home/server'

export default function Home (props) {
    const { cmsPage, storeConfig } = props
    const { title, metaTitle, metaDescription, metaKeywords } = cmsPage as CmsPageType
    const { defaultTitle, defaultKeywords, defaultRobots } = storeConfig

    return (
        <>
            <Head>
                <title>{metaTitle || title || 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'>
                <h1>Esta es una nueva sección en la página de Inicio</h1>
                <p>Bloque de ejemplo para demostrar como sobreescribir una página.</p>
            </section>
            <section className='pt-8 pb-10'>
                <CmsContent content={cmsPage.content} />
            </section>
        </>
    )
}

Componentes

Mercury tiene su propia colección de componentes (ver documentación temas) que se usan a lo largo de toda la aplicación.

Muchos de estos componentes se puede personalizar a través de las opciones de configuración en el fichero mercury.config.js, y a través de los ajustes de estilos con el fichero theme.css.

Para cuando la configuración y el ajuste de estilos del componente no sea suficiente para conseguir la personalización deseada nos queda la opción de poder crear nuestro propio componente dentro de aplicación, lo que sería un “Custom Component”.

Los componentes propios de la aplicación los crearemos dentro de un “Custom Theme”. La estructura de direcotios sería la seiguiente:

Estructura directorio de componentes de la aplicación.

Crear un Custom Component

Crearemos los “Custom Components” dentro del directorio mercury/src/component/CustomTheme.

Nuestro nuevo componente puede importar otros componentes, hooks, etc… de Mercury o de nuestra propia aplicación.

En el ejemplo de a continuación creams una personalización de la “Card” de producto basada en el componente original de Mercury.

mercury/src/component/CustomTheme/CustomCard.tsx
import { useI18n } from '@mercury/i18n'
import { useProductPrice } from '@mercury/service-adobe-commerce'
import { AddToCart, CardProductProps, Price, addToFormatClasses } from '@mercury/theme/components'
import { Badge, Card, Typography } from '@mercury/ui'
import Image from 'next/image'
import { useEffect, useState } from 'react'

export default function CustomCard ({
  product,
  addToFormat = 'vertical',
  border = 'none',
  buttonSize,
  hover = 'border',
  nameSize = 'h6',
  nameWeight = 'bold',
  radius = 'none',
  shadow = 'lg',
  showBadge = false,
  size = 'sm',
  ...props
}: CardProductProps) {
  const { t } = useI18n()
  const { sku, name, image, urlKey } = product
  const { price, loading: priceLoading } = useProductPrice({ sku })
  const [badge, setBadge] = useState<{text: string, color: string, variant?: string}>()

  useEffect(() => {
    if (showBadge && price && price.finalPrice < price.regularPrice) {
      setBadge({
        text: t('productPage.sale'),
        color: 'green'
      })
    }
  }, [price, setBadge, showBadge, t])

  return (
    <>
      <Card
        size={size}
        border={border}
        radius={radius}
        shadow={shadow}
        hover={hover}
        className='px-3 py-2 bg-white md:px-4'
        {...props}
      >
        <div>
          <div className='relative w-full aspect-square md:mb-4 lg:mb-6'>
            { badge &&
              <Badge variant={badge.variant ? badge.variant : 'default'}
                     color={badge.color ? badge.color : 'default'}
                     style={ { position: 'absolute', zIndex: 1 } }>
                {badge.text}
              </Badge>
            }
            <a href={`/product/${urlKey}`} className='relative block w-full h-full'>
              <Image
                  src={image.src}
                  alt={name}
                  placeholder='blur'
                  blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mPcXA8AAesBNGQg4IAAAAAASUVORK5CYII="
                  quality={60}
                  className='object-contain'
                  fill
                />
            </a>

          </div>

          { sku && <h6 className='hidden mb-2 text-gray-400 text'>{t('productPage.ref')}: {sku}</h6> }
          <Typography htmlTag='h6' variant={nameSize} weight={nameWeight} customClass='mb-4 line-clamp-3'>
            <a href={`/product/${urlKey}`}>{name}</a>
          </Typography>

        </div>

        <div>
          <div className={addToFormatClasses[addToFormat]}>
            <div className='md:min-h-[30px] md:overflow-hidden price-wrapper price-carousel'>
              <Price variants='h5 h6' price={price?.finalPrice} regularPrice={price?.regularPrice} loading={priceLoading} />
            </div>

            <div className='mt-4'>
              <AddToCart
                buttonVariant='primary'
                buttonSize={buttonSize}
                nextJSLinkHref={{
                  pathname: `/product/${urlKey}`,
                  query: product.variants ? null : { added: 'true' }
                }}
              >
                <span className='hidden md:inline'>{t('cart.add')}</span>
                <span className='inline md:hidden'>{t('cart.addMobile')}</span>
              </AddToCart>
            </div>
          </div>
        </div>
      </Card>
    </>
  )
}

Una vez tenemos creado nuestro “Custom Component” deberemos indicar a Mercury que use nuestro componente en lugar del suyo.

mercury/src/component/CustomTheme/CustomCard.tsx
import '@mercury/theme/styles'
import Mercury from '@mercury/theme/configs'
import NextNProgress from 'nextjs-progressbar'
import { Layout } from '@mercury/theme/components'
import { ApiProvider } from '@mercury/service-adobe-commerce'
import ContextProvider, { MercuryContext } from '@mercury/theme/features'
import 'glider-js/glider.min.css'

import '../styles/theme.css'

// Importamos MercuryConfigType para poder sobrescribir la configuración por defecto.
import { MercuryConfigType } from '@mercury/theme/src/features/MercuryContext'

// Importamos nuestro Custom Component.
import CustomCard from '@/components/CustomTheme/CustomCard'

// Inidicamos el componente que queremos substituir con el nuestro.
const mercuryConfig: MercuryConfigType = {
  ProductCardComponent: CustomCard 
}

function MyApp ({ Component, pageProps }) {
  const getLayout = Component.getLayout || ((page, pageProps) => <Layout pageProps={pageProps}>{page}</Layout>)

  return (
    <MercuryContext.Provider value={mercuryConfig}>
      <ApiProvider pageProps={pageProps}>
        <ContextProvider pageProps={pageProps}>
          {getLayout(
            <>
              <NextNProgress color={Mercury.components.pageLoader.color} />
              <Component {...pageProps} />
            </>,
            pageProps
          )}
        </ContextProvider>
      </ApiProvider>
    </MercuryContext.Provider>
  )
}

export default MyApp

También podemos usar nuestros “Custom Components” directamente en las páginas que hayamos sobreescrito. En estos casos no haría falta declarar la sobreescritura en el archivo mercury/src/pages/_app.tsx.