Construction de boutiques sans tête avec WooGraphQL: Chapitre 2 de 5

banner

En haut : ce tutoriel couvre une version WooGraphQL antérieure et n’est plus à jour. Une version réécrite est en cours — nous y lierons une fois qu’elle sera publiée.

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

Sommaire

  1. Configuration WordPress pour votre application eCommerce sans tête
  2. Création et mise en place de votre application eCommerce pour le développement
  3. Pages d’annonce de boutique de construction avec WooGraphQL
  4. Création de connexion utilisateur et navigation
  5. Création de la page Produit et des options de panier

Bienvenue dans la deuxième partie de notre série de tutoriels sur la création d’une application eCommerce sans tête! Ce chapitre suivant est de configurer l’application e-commerce pour l’interface avec votre magasin. Nous allons utiliser Next.js pour le front-end et profiter de plusieurs autres outils comme Schadcn/ui pour les composants, et Générateur de code GraphQL pour gérer les opérations de GraphQL.

Création et mise en place de notre application e-commerce pour le développement

Après avoir configuré votre magasin WooCommerce avec WooGraphQL, vous devriez être laissé avec un point d’arrêt GraphQL à https://site-name/graphql, Si vous utilisez la configuration de développement groupée du dernier chapitre http://localhost:8080/wp/graphql. Avec ça dit, commençons.

Créer l’application Next.js

Commençons par créer notre application Next.js et mettre en place quelques configurations nécessaires

1. D’abord, générer une nouvelle application Next.js en exécutant la commande suivante dans votre terminal: npx create-next-app@latest et suivez les instructions et naviguez vers le nouveau répertoire de projet.

2. Ensuite, installer le dotenv-flow et deepmerge paquets npm en exécutant la commande suivante dans votre terminal : npm i dotenv-flow deepmerge

3. Créer un nouveau .env.local fichier dans le répertoire racine de votre projet et le remplir avec les détails suivants:

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

N’oubliez pas de remplacer les valeurs entre crochets par vos équivalents respectifs.

4. Créer un nouveau next.config.js fichier dans le répertoire racine de votre projet et ajouter le code suivant:

/**
 * @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. Mettre à jour content propriété dans votre tailwind.config.js fichier à:

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

Installer shadcn/ui composants

Nous utiliserons les shadcn/ui bibliothèque pour les composants d’interface utilisateur de notre application. Voici comment l’installer et le configurer.

1. Exécutez la commande shadcn-ui init : npx shadcn-ui@latest init Puis, suivez les instructions.

2. Ouvrir le components.json fichier qui a été généré et remplacer son contenu par ce qui suit:

{
    "$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. Renommer lib/utils.ts fichier vers /utils/ui.ts et créer un /ui répertoire.

4. Ajoutez les composants nécessaires à votre projet en exécutant les commandes suivantes dans votre 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

Créer d’autres composants d’assurance-chômage

Nous allons créer des composants supplémentaires pour notre application.

1. Créer un nouveau ui/Image/index.ts fichier et ajouter le code suivant:

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

2. Créer un nouveau ui/Image/Image.tsx fichier et ajouter le code suivant:

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. Créer un nouveau ui/LoadingSpinner/index.ts fichier et ajouter le code suivant:

    export * from './LoadingSpinner';

4. Créer un nouveau ui/LoadingSpinner/LoadingSpinner.tsx fichier et ajouter le code suivant:

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. Créer un nouveau ui/NavLink/index.ts fichier et ajouter le code suivant:

export * from './NavLink';

6. Créer un nouveau ui/NavLink/NavLink.tsx fichier et ajouter le code suivant:

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>
    );
}

Installer et configurer GraphQL Code Generator

Enfin, nous devons configurer GraphQL Code Generator (Codegen) pour générer des opérations GraphQL dactylographiées. Codegen est un économiseur en temps réel et un must si vous travaillez avec beaucoup de composants ou avec un grand schéma GraphQL. En fonction de sa configuration, il générera du code lié au paramètre fourni par GraphQL. Dans notre application, il sera utilisé pour générer des types TypeScript pour le paramètre GraphQL, des types TS pour les opérations GraphQL que nous écrirons, et un wrapper soigné qui fera courir ces opérations une brise en utilisant le graphql-request bibliothèque.

1. Installez GraphQL Code Generator avec certains plugins nécessaires en exécutant la commande suivante dans votre terminal:

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

2. Créer un nouveau codegen.ts fichier dans le répertoire racine de votre projet et ajouter le code suivant:

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. Mettre à jour scripts propriété dans votre package.json fichier à inclure des scripts pour générer les opérations de GraphQL et les wrappers:

{
    ...
    "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. Créer un nouveau graphql/index.ts fichier et ajouter le code suivant:

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

5. Créer un nouveau graphql/main.graphql fichier et ajouter le code suivant:

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. Créer un nouveau graphql/client.ts fichier et ajouter le code suivant:

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!!!');
    }
}

Conclusion

Félicitations ! Vous avez réussi à configurer une application Next.js pour le développement du commerce électronique en utilisant le paramètre GraphQL de votre magasin WooCommerce. Vous avez installé et configuré shadcn/ui composants, créé d’autres composants UI, et installé et configuré le générateur de code GraphQL. Votre application est maintenant prête pour un développement et une intégration plus poussés avec votre magasin WooCommerce en utilisant GraphQL.

Continuer au chapitre suivant



Leave a Reply

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