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.
Sommaire
- Configuration WordPress pour votre application eCommerce sans tête
- Création et mise en place de votre application eCommerce pour le développement
- Pages d’annonce de boutique de construction avec WooGraphQL
- Création de connexion utilisateur et navigation
- 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 toastCré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-request2. 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.

Leave a Reply