Building Headless Shops with WooGraphQL: Capítulo 2 de 5

banner

Cabeza arriba: este tutorial cubre una versión anterior de WooGraphQL y ya no está al día. Una versión reescrita está en progreso — nos vincularemos a ella desde aquí una vez que sea publicada.

https://www.youtube.com/embed/nhlexP-KtSo

Cuadro de contenidos

  1. Configuración de WordPress para su aplicación de eCommerce sin cabeza
  2. Crear y configurar su aplicación eCommerce para el desarrollo
  3. Páginas de listado de edificios con WooGraphQL
  4. Creación de sesión de usuario y navegación
  5. Creación de la página del producto y Opciones del carro

Bienvenido a la segunda parte de nuestra serie tutorial sobre la creación de una aplicación de eCommerce sin cabeza! Este próximo capítulo es configurar la aplicación de comercio electrónico para interactuar con su tienda. Aprovecharemos Next.js para el front-end y aprovecharemos varias otras herramientas como shadcn/ui para componentes, y Generador de códigos de grafQL para manejar operaciones de GraphQL.

Creación y creación de nuestra aplicación de comercio electrónico para el desarrollo

Después de configurar su tienda WooCommerce con WooGraphQL, debe quedar con un punto final de GraphQL en https://site-name/graphql, Si usted está usando la configuración de desarrollo agrupado del último capítulo esto es http://localhost:8080/wp/graphqlCon eso dicho, empecemos.

Crear la aplicación Next.js

Empecemos por crear nuestra aplicación Next.js y configurar algunas configuraciones necesarias

1. Primero, generar una nueva aplicación Next.js ejecutando el siguiente comando en su terminal: npx create-next-app@latest y seguir las indicaciones y navegar hasta el directorio de proyecto recién creado.

2. Luego, instalar el dotenv-flow y deepmerge npm paquetes ejecutando el siguiente comando en su terminal: npm i dotenv-flow deepmerge

3. Crear un nuevo .env.local archivo en el directorio raíz de su proyecto y llenarlo con los siguientes detalles:

GRAPHQL_ENDPOINT=[GRAPHQL_ENDPOINT]
FRONTEND_URL=[NEXT_APP_URL]
BACKEND_URL=[WP_URL]
SITE_NAME=[SITE_NAME]
SITE_DESCRIPTION=[SITE_DESCRIPTION]

No olvides reemplazar los valores entre corchetes con tus respectivos equivalentes.

4. Crear un nuevo next.config.js archivo en el directorio raíz de su proyecto y añadir el siguiente código:

/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
    reactStrictMode: true,
    swcMinify: true,
    images: {
        dangerouslyAllowSVG: true,
        formats: ['image/avif', 'image/webp'],
        domains: ['localhost'],
        minimumCacheTTL: 60,
        disableStaticImages: true,
    },
    env: {
        GRAPHQL_ENDPOINT: process.env.GRAPHQL_ENDPOINT,
        FRONTEND_URL: process.env.FRONTEND_URL,
        BACKEND_URL: process.env.BACKEND_URL,
        SITE_NAME: process.env.SITE_NAME,
        SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
    },
}

module.exports = nextConfig;

5. Actualizar content propiedad en su tailwind.config.js file to the following:

...
content: [
    './ui/**/*.{ts,tsx}',
    './server/**/*.{ts,tsx}',
    './client/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
]
...

Instala shadcn/ui componentes

Vamos a usar el shadcn/ui biblioteca para los componentes UI de nuestra aplicación. Así es como instalarlo y configurarlo.

1. Ejecute el comando shadcn-ui init: npx shadcn-ui@latest init Entonces, sigue los avisos.

2. Abrir el components.json archivo que se generó y sustituir su contenido por el siguiente:

{
    "$schema": "https://ui.shadcn.com/schema.json",
    "style": "new-york",
    "rsc": true,
    "tailwind": {
        "config": "tailwind.config.js",
        "css": "app/globals.css",
        "baseColor": "slate",
        "cssVariables": true
    },
    "aliases": {
        "components": "/",
        "utils": "@/utils/ui"
    }
}

3. Renombrar el lib/utils.ts archivo a /utils/ui.ts y crear un /ui directorio.

4. Agregue los componentes necesarios a su proyecto ejecutando los siguientes comandos en su terminal:

npx shadcn-ui@latest add aspect-ratio
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add form
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npx shadcn-ui@latest add radio-group
npx shadcn-ui@latest add select
npx shadcn-ui@latest add sheet
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add toast

Crear otros componentes UI

Crearemos algunos componentes adicionales para nuestra aplicación.

1. Crear un nuevo ui/Image/index.ts archivar y añadir el siguiente código:

    'use client';
    export * from './Image';

2. Crear un nuevo ui/Image/Image.tsx archivar y añadir el siguiente código:

import { useState } from 'react';
import NextImage from 'next/image';
import { cn } from '@/utils/ui';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { AspectRatio } from "@/ui/aspect-ratio"

export type ImageProps = {
    className?: string;
    src: string;
    sizes?: string;
    width?: number;
    height?: number;
    ratio?: number;
    alt: string;
    style?: JSX.IntrinsicElements['img']['style']
    fill?: boolean;
    priority?: boolean;
}

export function Image(props: ImageProps) {
    const [isLoading, setLoading] = useState(true);

    const {
        className = '',
        src,
        alt,
        sizes,
        width,
        height,
        ratio,
        style,
        fill = true,
        priority,
    } = props;

    return (
        <div
            className={cn(
                'overflow-hidden group relative',
                className && className,
            )}
            style={style}
>
            <AspectRatio ratio={ratio}>
                {isLoading && (
                    <LoadingSpinner className="position absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"/>
                )}
                <NextImage
                    src={src}
                    alt={alt}
                    width={width as number}
                    height={height as number}
                    sizes={sizes}
                    className={cn(
                        'group-hover:opacity-75 object-cover',
                        'duration-700 ease-in-out',
                        isLoading
                        ? 'grayscale blur-2xl scale-110'
                        : 'grayscale-0 blur-0 scale-100',
                    )}
                    fill={fill}
                    onLoadingComplete={() => setLoading(false)}
                    priority={priority}
/>
            </AspectRatio>
        </div>
    );
}

3. Crear un nuevo ui/LoadingSpinner/index.ts archivar y añadir el siguiente código:

    export * from './LoadingSpinner';

4. Crear un nuevo ui/LoadingSpinner/LoadingSpinner.tsx archivar y añadir el siguiente código:

export interface LoadingSpinnerProps {
    className?: string;
    noText?: boolean;
    color?: string;
}

export function LoadingSpinner({ className = '', noText = false, color = 'amber-400' }: LoadingSpinnerProps) {
    return (
        <div aria-label="Loading..." role="status" className={`relative flex items-center justify-center space-x-2 ${className}`}>
        <svg className="h-6 w-6 animate-spin stroke-amber-600" viewBox="0 0 256 256">
            <line x1="128" y1="32" x2="128" y2="64" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
            <line
                x1="195.9"
                y1="60.1"
                x2="173.3"
                y2="82.7"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="24"
/>
            <line x1="224" y1="128" x2="192" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
            <line
                x1="195.9"
                y1="195.9"
                x2="173.3"
                y2="173.3"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="24"
/>
            <line x1="128" y1="224" x2="128" y2="192" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
            <line
                x1="60.1"
                y1="195.9"
                x2="82.7"
                y2="173.3"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="24"
/>
            <line x1="32" y1="128" x2="64" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
            <line
                x1="60.1"
                y1="60.1"
                x2="82.7"
                y2="82.7"
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="24"
/>
        </svg>
            {!noText && <span className="text-xs font-medium text-gray-500">Loading...</span>}
        </div>
    );
}

5. Crear un nuevo ui/NavLink/index.ts archivar y añadir el siguiente código:

export * from './NavLink';

6. Crear un nuevo ui/NavLink/NavLink.tsx archivar y añadir el siguiente código:

import { PropsWithChildren } from 'react';
import Link from 'next/link';
import { cn } from '@woographql/utils/ui';

export const linkClassName = 'transition-colors group-hover:text-blue-400';

export interface NavLinkProps {
    href: string;
    className?: string;
    shallow?: boolean;
    prefetch?: boolean;
}

export function NavLink(props: PropsWithChildren<NavLinkProps>) {
    const {
        children,
        href,
        className,
        shallow,
        prefetch,
    } = props;
    return (
        <Link
            className={cn(
                className,
                linkClassName,
            )}
            href={href}
            shallow={shallow}
            prefetch={prefetch}
>
            {children}
        </Link>
    );
}

Instalar y configurar el generador de códigos GraphQL

Por último, necesitamos establecer el Generador de Códigos GraphQL (Codegen) para generar operaciones tipo GraphQL. Codegen es un ahorro de tiempo real y una necesidad si trabaja con muchos componentes o un esquema GraphQL grande. Basado en cómo se configura, generará código relacionado con el punto final de GraphQL proporcionado. En nuestra aplicación se utilizará para generar tipos de TipoScript para el endpoint de GraphQL, tipos de TS para las operaciones de GraphQL que escribiremos, y envoltura limpia que hará funcionar dichas operaciones una brisa usando la graphql-request biblioteca.

1. Instalar el Generador de Código de GraphQL junto con algunos plugins requeridos ejecutando el siguiente comando en su terminal:

npm i -D npm-run-all @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request

2. Crear un nuevo codegen.ts archivo en el directorio raíz de su proyecto y añadir el siguiente código:

import type { CodegenConfig } from '@graphql-codegen/cli';
import dotenv from 'dotenv-flow';

dotenv.config({ silent: true }); 
const config: CodegenConfig = {
    schema: process.env.GRAPHQL_ENDPOINT,
    documents: ['graphql/**/*.graphql'],
    verbose: true,
    overwrite: true,
    generates: {
        'graphql/generated.ts': {
            plugins: [
                'typescript',
                'typescript-operations',
                'typescript-graphql-request',
            ],
            config: {
                namingConvention: 'keep',
            }
        }
    }
}
export default config;

3. Actualizar scripts propiedad en su package.json archivo para incluir scripts para generar las operaciones y envolturas de GraphQL:

{
    ...
    "scripts": {
        "next:dev": "next dev",
        "next:build": "next build",
        "start": "next start",
        "lint": "next lint",
        "codegen:dev": "graphql-codegen --config codegen.ts --watch",
        "codegen:build": "graphql-codegen --config codegen.ts",
        "dev": "run-p codegen:dev next:dev",
        "build": "NODE_ENV=production run-s codegen:build next:build",
    },
    ...
}

4. Crear un nuevo graphql/index.ts archivar y añadir el siguiente código:

 export * from './generated';
 export * from './client';

5. Crear un nuevo graphql/main.graphql archivar y añadir el siguiente código:

fragment MenuItemContent on MenuItem {
  id
  uri
  title
  label
  cssClasses
}

fragment MenuItemRecursive on MenuItem {
  ...MenuItemContent
  childItems {
    nodes {
      ...MenuItemContent
    }
  }
}

fragment MenuContent on Menu {
  id
  name
  locations
  slug
  menuItems(first: 20, where: {parentId: 0}) {
    nodes {
      ...MenuItemRecursive
    }
  }
}

fragment CustomerContent on Customer {
  id
  sessionToken
  firstName
  shipping {
    postcode
    state
    city
    country
  }
}

fragment ProductContentSlice on Product {
  id
  databaseId
  name
  slug
  type
  image {
    id
    sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
    altText
  }
  ... on SimpleProduct {
    price
    regularPrice
    soldIndividually
  }
  ... on VariableProduct {
    price
    regularPrice
    soldIndividually
  }
}

fragment ProductVariationContentSlice on ProductVariation {
  id
  databaseId
  name
  slug
  image {
    id
    sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
    altText
  }
  price
  regularPrice
}

fragment ProductContentSmall on Product {
  id
  databaseId
  slug
  name
  type
  shortDescription(format: RAW)
  image {
    id
    sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
    altText
  }
  productCategories(first: 20) {
    nodes {
      id
      slug
      name
    }
  }
  productTags(first: 20) {
    nodes {
      id
      slug
      name
    }
  }
  allPaColor(first: 100) {
    nodes {
      id
      slug
      name
    }
  }
  ... on SimpleProduct {
    onSale
    stockStatus
    price
    rawPrice: price(format: RAW)
    regularPrice
    salePrice
    soldIndividually
  }
  ... on VariableProduct {
    onSale
    stockStatus
    price
    rawPrice: price(format: RAW)
    regularPrice
    salePrice
    soldIndividually
  }
}

fragment ProductContentFull on Product {
  id
  databaseId
  slug
  name
  type
  description
  shortDescription(format: RAW)
  image {
    id
    sourceUrl
    altText
  }
  galleryImages {
    nodes {
      id
      sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
      altText
    }
  }
  productTags(first: 20) {
    nodes {
      id
      slug
      name
    }
  }
  attributes {
    nodes {
      id
      attributeId
      ... on LocalProductAttribute {
        name
        label
        options
        variation
      }
      ... on GlobalProductAttribute {
        name
        label
        options
        variation
        slug
        terms(first: 100) {
          nodes {
            id
            name
            slug
          }
        }
      }
    }
  }
  ... on SimpleProduct {
    onSale
    stockStatus
    price
    rawPrice: price(format: RAW)
    regularPrice
    salePrice
    stockStatus
    stockQuantity
    soldIndividually
    defaultAttributes(first: 100) {
      nodes {
        id
        attributeId
        name
        value
        label
      }
    }
  }
  ... on VariableProduct {
    onSale
    price
    rawPrice: price(format: RAW)
    regularPrice
    salePrice
    stockStatus
    stockQuantity
    soldIndividually
    defaultAttributes(first: 100) {
      nodes {
        id
        attributeId
        label
        name
        value
      }
    }
    variations(first: 50) {
      nodes {
        id
        databaseId
        name
        price
        rawPrice: price(format: RAW)
        regularPrice
        salePrice
        onSale
        attributes {
          nodes {
            name
            label
            value
          }
        }
        image {
          id
          sourceUrl
          altText
        }
      }
    }
  }
}

fragment VariationContent on ProductVariation {
  id
  name
  slug
  price
  regularPrice
  salePrice
  stockStatus
  stockQuantity
  onSale
  image {
    id
    sourceUrl
    altText
  }
}

fragment CartItemContent on CartItem {
  key
  product {
    node {
      ...ProductContentSlice
    }
  }
  variation {
    attributes {
      id
      label
      name
      value
    }
    node {
      ...ProductVariationContentSlice
    }
  }
  quantity
  total
  subtotal
  subtotalTax
  extraData {
    key
    value
  }
}

fragment CartContent on Cart {
  contents(first: 100) {
    itemCount
    nodes {
      ...CartItemContent
    }
  }
  appliedCoupons {
    code
    discountAmount
    discountTax
  }
  needsShippingAddress
  availableShippingMethods {
    packageDetails
    supportsShippingCalculator
    rates {
      id
      instanceId
      methodId
      label
      cost
    }
  }
  subtotal
  subtotalTax
  shippingTax
  shippingTotal
  total
  totalTax
  feeTax
  feeTotal
  discountTax
  discountTotal
}

fragment CustomerFields on Customer {
    id
    databaseId
    firstName
    lastName
    displayName
    email
    sessionToken
    metaData {
      key
      value
    }
}

query GetTopNav {
  menu(id: "primary", idType: LOCATION) {
    ...MenuContent
  }
}

query GetProducts($first: Int, $after: String, $where: RootQueryToProductConnectionWhereArgs) {
  products(first: $first, after: $after, where: $where) {
    pageInfo {
      endCursor
      hasNextPage
    }
    edges {
      cursor
      node {
        ...ProductContentSmall
      }
    }
    nodes {
      ...ProductContentSmall
    }
  }
}

query GetProduct($id: ID!, $idType: ProductIdTypeEnum) {
  product(id: $id, idType: $idType) {
    ...ProductContentFull
  }
}

query GetShopCategories($first: Int, $after: String, $where: RootQueryToProductCategoryConnectionWhereArgs) {
  productCategories(first: $first, after: $after, where: $where) {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        name
        slug
      }
    }
    nodes {
      id
      name
      slug
    }
  }
}

query GetShopTags($first: Int, $after: String, $where: RootQueryToProductTagConnectionWhereArgs) {
  productTags(first: $first, after: $after, where: $where) {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        name
        slug
      }
    }
    nodes {
      id
      name
      slug
    }
  }
}

query GetShopColors($first: Int, $after: String, $where: RootQueryToPaColorConnectionWhereArgs) {
  allPaColor(first: $first, after: $after, where: $where) {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        name
        slug
      }
    }
    nodes {
      id
      name
      slug
    }
  }
}

query GetSession {
  cart {
    ...CartContent
  }
  customer {
    ...CustomerContent
  }
}

mutation AddToCart(
  $productId: Int!,
  $variationId: Int,
  $quantity: Int,
  $variation: [ProductAttributeInput],
  $extraData: String
) {
  addToCart(
    input: {
      productId: $productId,
      variationId: $variationId,
      quantity: $quantity,
      variation: $variation,
      extraData: $extraData
    }
  ) {
    cart {
      ...CartContent
    }
    cartItem {
      ...CartItemContent
    }
  }
}

mutation UpdateCartItemQuantities($items: [CartItemQuantityInput]) {
  updateItemQuantities(input: {items: $items}) {
    cart {
      ...CartContent
    }
    items {
      ...CartItemContent
    }
  }
}

mutation RemoveItemsFromCart($keys: [ID], $all: Boolean) {
  removeItemsFromCart(input: {keys: $keys, all: $all}) {
    cart {
      ...CartContent
    }
    cartItems {
      ...CartItemContent
    }
  }
}

mutation Login($username: String!, $password: String!) {
  login(input: { username: $username, password: $password }) {
    authToken
    refreshToken
    customer {
      ...CustomerFields
    }
  }
}

mutation RefreshAuthToken($refreshToken: String!) {
  refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
    authToken
  }
}

mutation UpdateSession($input: UpdateSessionInput!) {
  updateSession(input: $input) {
    session {
      id
      key
      value
    }
  }
}

6. Crear un nuevo graphql/client.ts archivar y añadir el siguiente código:

import { GraphQLClient } from 'graphql-request';
import deepmerge from 'deepmerge';

import {
    RootQueryToProductConnectionWhereArgs,
    Product,
    ProductCategory,
    PaColor,
    getSdk,
    RootQueryToProductCategoryConnectionWhereArgs,
    RootQueryToPaColorConnectionWhereArgs,
    ProductIdTypeEnum,
} from './generated';

let client: GraphQLClient;
export function getClient() {
    const endpoint = process.env.GRAPHQL_ENDPOINT
    if (!endpoint) {
        throw new Error('GRAPHQL_ENDPOINT is not defined')
    }

    if (!client) {
        client = new GraphQLClient(endpoint);
    }

    return client;
}

export function getClientWithSdk() {
    return getSdk(getClient());
}

const initialConnectionResults = {
    pageInfo: {
        hasNextPage: true,
        endCursor: null,
    },
    edges: [],
    nodes: [],
};

export async function fetchProducts(
    pageSize: number, 
    pageLimit = 0,
    where?: RootQueryToProductConnectionWhereArgs
) {
    try {
        const client = getClientWithSdk();
        let data = { products: initialConnectionResults }
        let after = '';
        let count = 0
        while(data.products.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
            const next = await client.GetProducts({
                first: pageSize,
                after,
                where,
            });

            data = deepmerge(data, next);
            after = next.products?.pageInfo.endCursor || '';
            count++;
        }

        return (data.products.nodes) as Product[];
    } catch (err) {
        console.error(err || 'Failed to fetch product listing!!!');
    }
}

export async function fetchCategories(
    pageSize: number,
    pageLimit = 0,
    where?: RootQueryToProductCategoryConnectionWhereArgs
) {
    try {
        const client = getClientWithSdk();
        let data = { productCategories: initialConnectionResults }
        let after = '';
        let count = 0
        while(data.productCategories.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
        const next = await client.GetShopCategories({
            first: pageSize,
            after,
            where,
        });

            data = deepmerge(data, next);
            after = next.productCategories?.pageInfo.endCursor || '';
            count++;
        }

        return (data.productCategories.nodes) as ProductCategory[];

    } catch (err) {
        console.error(err || 'Failed to fetch product categories!!!');
    }
}

export async function fetchColors(
    pageSize: number,
    pageLimit = 0,
    where?: RootQueryToPaColorConnectionWhereArgs
) {
    try {
        const client = getClientWithSdk();
        let data = { allPaColor: initialConnectionResults }
        let after = '';
        let count = 0
        while(data.allPaColor.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
        const next = await client.GetShopColors({
            first: pageSize,
            after,
            where
        });

            data = deepmerge(data, next);
            after = next.allPaColor?.pageInfo.endCursor || '';
            count++;
        }

        return (data.allPaColor.nodes) as PaColor[];
    } catch (err) {
        console.error(err || 'Failed to fetch product color attributes!!!');
    }
}

export async function fetchProductBy(slug: string, idType: ProductIdTypeEnum) {
    try {
        const client = getClientWithSdk();
        const data = await client.GetProduct({
            id: slug,
            idType: idType,
        });

        if (!data.product) {
            throw new Error('Product not found!!!');
        }

        return data.product as Product;
    } catch (err) {
        console.error(err || 'Failed to fetch product data!!!');
    }
}

Conclusión

¡Enhorabuena! Usted ha establecido con éxito una aplicación Next.js para el desarrollo del comercio electrónico utilizando el punto final GraphQL de su tienda WooCommerce. Has instalado y configurado shadcn/ui componentes, crearon otros componentes de la UI, e instalaron y configuraron el Generador de Códigos GraphQL. Su aplicación está lista para mayor desarrollo e integración con su tienda WooCommerce usando GraphQL.

Continuar hasta el próximo capítulo



Leave a Reply

Your email address will not be published. Required fields are marked *