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.
Cuadro de contenidos
- Configuración de WordPress para su aplicación de eCommerce sin cabeza
- Crear y configurar su aplicación eCommerce para el desarrollo
- Páginas de listado de edificios con WooGraphQL
- Creación de sesión de usuario y navegación
- 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 toastCrear 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-request2. 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.

Leave a Reply