Next.js Приложение для электронной коммерции с помощью Strapi и Chakra UI


В этой статье мы создадим приложение для электронной коммерции из нашей админ-панели Strapi-Multitenancy, которую мы уже делали ранее.

Теперь она используется без головы с версией refine 3. Вы можете использовать любую библиотеку пользовательского интерфейса, которую вы хотите, с функцией headless.

Мы будем использовать Strapi и Chakra-UI вместе с Next.js в нашем примере клиентского приложения электронной коммерции.

Настройка проекта Refine

Давайте начнем с создания нашего проекта refine. Вы можете использовать супершаблон для создания проекта refine.

npx superplate-cli -p refine-nextjs refine-ecommerce-example
Вход в полноэкранный режим Выход из полноэкранного режима
✔ What will be the name of your app · refine-ecommerce-example
✔ Package manager: · npm
✔ Do you want to using UI Framework? > No(headless)
✔ Data Provider: Strapi
✔ i18n - Internationalization: · no
Войдите в полноэкранный режим Выйти из полноэкранного режима

superplate быстро создаст наш проект refine в соответствии с выбранными нами функциями. Продолжим установку пакетов refine Strapi-v4 Data Provider и Chakra-UI, которые мы будем использовать позже.

Установка

cd refine-ecommerce-example

npm i @pankod/refine-strapi-v4
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Войдите в полноэкранный режим Выйти из полноэкранного режима

Наш проект refine и установки теперь готовы! Давайте начнем его использовать.

Использование

Настройка Refine для Strapi-v4

pages/index.tsx:

import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
    const dataProvider = DataProvider(API_URL);

    return (
        <Refine
            routerProvider={routerProvider}
            dataProvider={dataProvider}
        >
            <Component {...pageProps} />
        </Refine>
    );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Настройка провайдера Chakra-UI

pages/index.tsx:

import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { ChakraProvider } from "@chakra-ui/react";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
    const dataProvider = DataProvider(API_URL);

    return (
        <Refine routerProvider={routerProvider} dataProvider={dataProvider}>
            <ChakraProvider>
                <Component {...pageProps} />
            </ChakraProvider>
        </Refine>
    );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Создание коллекций на Strapi

Мы создали три коллекции на Strapi: store, product и order и добавили связь между ними. Подробную информацию о том, как создать коллекцию, вы можете найти здесь.

Мы создали наши коллекции в предыдущем руководстве по Strapi Multitenancy. Теперь мы будем использовать те же коллекции.

Для получения подробной информации обратитесь к разделу Коллекции проекта. →

Создание макета Refine

refine headless не связан с каким-либо пользовательским интерфейсом. Настройка пользовательского интерфейса полностью зависит от вас. Давайте создадим простой макет для этого примера.

Макет, который мы создали сейчас, будет отображать только логотип refine. В следующих шагах мы отредактируем наш макет.

components/Layout.tsx:

import { Box, Container, Flex, Image } from "@chakra-ui/react";

export const Layout: React.FC = ({ children }) => {
    return (
        <Box
            display={"flex"}
            flexDirection={"column"}
            backgroundColor={"#eeeeee"}
            minH={"100vh"}
        >
            <Container maxW={"container.lg"}>
                <Flex justify={"space-between"} mt={4} alignSelf={"center"}>
                    <a href="https://refine.dev">
                        <Image alt="Refine Logo" src={"./refine_logo.png"} />
                    </a>
                </Flex>
                {children}
            </Container>
        </Box>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

pages/_app.tsx:

import React from "react";
import { AppProps } from "next/app";
import Head from "next/head";
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-nextjs-router";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { ChakraProvider } from "@chakra-ui/react";
import { Layout } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
    const dataProvider = DataProvider(API_URL);

    return (
        <Refine
            routerProvider={routerProvider}
            dataProvider={dataProvider}
            Layout={Layout}
        >
            <ChakraProvider>
                <Component {...pageProps} />
            </ChakraProvider>
        </Refine>
    );
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Дизайн карточки товара с помощью Chakra-UI

Давайте разработаем дизайн карточки товара с помощью Chakra-UI.

src/components/ProductCard.tsx

import React from "react";
import { Box, Image, Badge, Button } from "@chakra-ui/react";

export type ProductProps = {
    id: string;
    title: string;
    description: string;
    cardImage: string;
};

export const ProductCard: React.FC<ProductProps> = ({
    id,
    title,
    description,
    cardImage,
}) => {
    return (
        <Box maxH={"sm"} borderWidth="1px" borderRadius="lg" overflow="hidden">
            <Image w={"100%"} h={200} src={cardImage} />
            <Box p="6" bgColor={"gray.600"}>
                <Box display="flex" alignItems="baseline" mb={2} ml={-2}>
                    <Badge borderRadius="full" px="2" colorScheme="teal">
                        New Product
                    </Badge>
                </Box>

                <Box
                    mt="1"
                    fontWeight="semibold"
                    as="h4"
                    lineHeight="tight"
                    isTruncated
                    color={"white"}
                >
                    {title}
                </Box>

                <Box color={"white"}>{}</Box>
                <Box
                    color="white"
                    fontSize="sm"
                    display={"flex"}
                    mt={4}
                    justifyContent={"flex-end"}
                ></Box>
            </Box>
        </Box>
    );
};
Войти в полноэкранный режим Выход из полноэкранного режима

Мы создали наш компонент «Карточка товара». Теперь давайте перейдем к процессу получения и отображения наших продуктов из Strapi.

Выборка продуктов с помощью SSR

Сначала давайте получим наши продукты с помощью функции nextjs getServerSideProps.

GetServerSideProps.

pages/index.tsx:

import { GetServerSideProps } from "next";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct } from "interfaces";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

export const getServerSideProps: GetServerSideProps = async (context) => {
    const data = await DataProvider(API_URL).getList<IProduct>({
        resource: "products",
        metaData: { populate: ["image"] },
    });

    return {
        props: { products: data },
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

Создание списка продуктов с помощью Refine

Давайте обработаем данные, полученные выше, с помощью хука useTable от Refine. Затем поместим наши данные в компонент ProductCard.

pages/index.tsx:

import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct } from "interfaces";
import { SimpleGrid } from "@chakra-ui/react";
import { ProductCard } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
    products: GetListResponse<IProduct>;
};

export const ProductList: React.FC<ItemProps> = ({ products }) => {
    const { tableQueryResult } = useTable<IProduct>({
        resource: "products",
        queryOptions: {
            initialData: products,
        },
        metaData: { populate: ["image"] },
    });

    return (
        <LayoutWrapper>
            <SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
                {tableQueryResult.data?.data.map((item) => (
                    <ProductCard
                        id={item.id}
                        title={item.title}
                        description={item.description}
                        cardImage={
                            item.image
                                ? API_URL + item.image.url
                                : "./error.png"
                        }
                    />
                ))}
            </SimpleGrid>
        </LayoutWrapper>
    );
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
    const data = await DataProvider(API_URL).getList<IProduct>({
        resource: "products",
        metaData: { populate: ["image"] },
    });

    return {
        props: { products: data },
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление фильтрации на основе магазина

Выше мы получили все товары. Теперь давайте найдем магазины и выведем список товаров, относящихся к конкретным магазинам, отдельно.

Во-первых, давайте найдем наши магазины, используя уточняющий хук useMany в функции getServerSideProps. Далее мы создадим кнопки для магазинов. Когда эти кнопки будут нажаты, магазин будет выбран, мы выполним фильтрацию с помощью useTable setFilters и выведем список товаров, характерных для этого магазина.

pages/index.tsx:

export const getServerSideProps: GetServerSideProps = async (context) => {
    const data = await DataProvider(API_URL).getList<IProduct>({
        resource: "products",
        metaData: { populate: ["image"] },
        pagination: { current: 1, pageSize: 9 },
    });

    const { data: storesData } = await DataProvider(API_URL).getMany({
        resource: "stores",
        ids: ["1", "2", "3"],
    });

    return {
        props: {
            products: data,
            stores: storesData,
        },
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

pages/index.tsx:

import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct, IStore } from "interfaces";
import { Button, SimpleGrid, Flex, Text } from "@chakra-ui/react";
import { ProductCard, FilterButton } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
    products: GetListResponse<IProduct>;
    stores: IStore[];
};

export const ProductList: React.FC<ItemProps> = ({ products, stores }) => {
    const { tableQueryResult, setFilters } = useTable<IProduct>({
        resource: "products",
        queryOptions: {
            initialData: products,
        },
        metaData: { populate: ["image"] },
    });

    return (
        <LayoutWrapper>
            <Flex mt={6} gap={2}>
                <FilterButton
                    setFilters={() =>
                        setFilters([
                            {
                                field: "stores][id]",
                                operator: "eq",
                                value: undefined,
                            },
                        ])
                    }
                >
                    <Text fontSize={{ base: "12px", md: "14px", lg: "14px" }}>
                        All Products
                    </Text>
                </FilterButton>
                {stores?.map((item) => {
                    return (
                        <FilterButton
                            setFilters={() =>
                                setFilters([
                                    {
                                        field: "stores][id]",
                                        operator: "eq",
                                        value: item.id,
                                    },
                                ])
                            }
                        >
                            <Text
                                fontSize={{
                                    base: "12px",
                                    md: "14px",
                                    lg: "14px",
                                }}
                            >
                                {item.title}
                            </Text>
                        </FilterButton>
                    );
                })}
            </Flex>
            <SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
                {tableQueryResult.data?.data.map((item) => (
                    <ProductCard
                        id={item.id}
                        title={item.title}
                        description={item.description}
                        cardImage={
                            item.image
                                ? API_URL + item.image.url
                                : "./error.png"
                        }
                    />
                ))}
            </SimpleGrid>
        </LayoutWrapper>
    );
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
    const data = await DataProvider(API_URL).getList<IProduct>({
        resource: "products",
        metaData: { populate: ["image"] },
        pagination: { current: 1, pageSize: 9 },
    });

    const { data: storesData } = await DataProvider(API_URL).getMany({
        resource: "stores",
        ids: ["1", "2", "3"],
    });

    return {
        props: {
            products: data,
            stores: storesData,
        },
    };
};
Войти в полноэкранный режим Выход из полноэкранного режима

Добавление пагинации

Мы отображаем все товары на странице All Products. Давайте добавим пагинацию на эту страницу и разделим товары на страницы. Мы будем выполнять пагинацию с помощью свойств pageSize, current и setCurrent из хука useTable.

За подробной информацией обратитесь к документации по useTable. →

pages/index.tsx:

import { GetServerSideProps } from "next";
import { LayoutWrapper, GetListResponse, useTable } from "@pankod/refine-core";
import { DataProvider } from "@pankod/refine-strapi-v4";

import { IProduct, IStore } from "interfaces";
import { Button, SimpleGrid, Flex, Text } from "@chakra-ui/react";
import { ProductCard, FilterButton } from "src/components";

const API_URL = "https://api.strapi-multi-tenant.refine.dev/api";

type ItemProps = {
    products: GetListResponse<IProduct>;
    stores: IStore[];
};

export const ProductList: React.FC<ItemProps> = ({ products, stores }) => {
    const { tableQueryResult, setFilters, current, setCurrent, pageSize } =
        useTable<IProduct>({
            resource: "products",
            queryOptions: {
                initialData: products,
            },
            initialPageSize: 9,
            metaData: { populate: ["image"] },
        });

    const totalPageCount = Math.ceil(tableQueryResult.data?.total!! / pageSize);

    return (
        <LayoutWrapper>
            <Flex mt={6} gap={2}>
                <FilterButton
                    setFilters={() =>
                        setFilters([
                            {
                                field: "stores][id]",
                                operator: "eq",
                                value: undefined,
                            },
                        ])
                    }
                >
                    <Text fontSize={{ base: "12px", md: "14px", lg: "14px" }}>
                        All Products
                    </Text>
                </FilterButton>
                {stores?.map((item) => {
                    return (
                        <FilterButton
                            setFilters={() =>
                                setFilters([
                                    {
                                        field: "stores][id]",
                                        operator: "eq",
                                        value: item.id,
                                    },
                                ])
                            }
                        >
                            <Text
                                fontSize={{
                                    base: "12px",
                                    md: "14px",
                                    lg: "14px",
                                }}
                            >
                                {item.title}
                            </Text>
                        </FilterButton>
                    );
                })}
            </Flex>
            <SimpleGrid columns={[1, 2, 3]} mt={6} spacing={3}>
                {tableQueryResult.data?.data.map((item) => (
                    <ProductCard
                        id={item.id}
                        title={item.title}
                        description={item.description}
                        cardImage={
                            item.image
                                ? API_URL + item.image.url
                                : "./error.png"
                        }
                    />
                ))}
            </SimpleGrid>
            <Flex justify={"flex-end"} mt={4} mb={4} gap={2}>
                {Array.from(Array(totalPageCount), (e, i) => {
                    if (current > totalPageCount) {
                        setCurrent(i);
                    }
                    return (
                        <Button
                            colorScheme={"teal"}
                            onClick={() => setCurrent(i + 1)}
                        >
                            {"Page: " + (i + 1)}
                        </Button>
                    );
                })}
            </Flex>
        </LayoutWrapper>
    );
};

export default ProductList;

export const getServerSideProps: GetServerSideProps = async (context) => {
    const data = await DataProvider(API_URL).getList<IProduct>({
        resource: "products",
        metaData: { populate: ["image"] },
        pagination: { current: 1, pageSize: 9 },
    });

    const { data: storesData } = await DataProvider(API_URL).getMany({
        resource: "stores",
        ids: ["1", "2", "3"],
    });

    return {
        props: { products: data, stores: storesData },
    };
};
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление функций корзины и оплаты с помощью Snipcart

Одним из этапов, которые должны быть в приложении электронной коммерции, являются операции с корзиной и платежами. В нашем примере мы будем использовать Snipcart для этого процесса.

Для получения подробной информации обратитесь к документации Snipcart. →

Установка виджета Snipcart

pages/_app.tsx:

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
    const dataProvider = DataProvider(API_URL);

    return (
        <>
            <Head>
                <link rel="preconnect" href="https://app.snipcart.com" />
                <link
                    rel="stylesheet"
                    href="https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.css"
                />
                <script
                    async
                    src="https://cdn.snipcart.com/themes/v3.0.16/default/snipcart.js"
                />
            </Head>
            <Refine
                routerProvider={routerProvider}
                dataProvider={dataProvider}
                resources={[{ name: "products" }]}
                Layout={Layout}
            >
                <ChakraProvider>
                    <Component {...pageProps} />
                </ChakraProvider>
            </Refine>
            <div hidden id="snipcart" data-api-key="YOUR_SNIPCART_TEST_KEY" />
        </>
    );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление кнопки «Добавить в корзину» в компонент ProductCard

src/components/ProductCard.tsx:

import React from "react";
import { Box, Image, Badge, Button } from "@chakra-ui/react";

export type ProductProps = {
    id: string;
    title: string;
    description: string;
    cardImage: string;
};

export const ProductCard: React.FC<ProductProps> = ({
    id,
    title,
    description,
    cardImage,
}) => {
    return (
        <Box
            maxH={"sm"}
            maxW="sm"
            borderWidth="1px"
            borderRadius="lg"
            overflow="hidden"
        >
            <Image w={"100%"} h={200} src={cardImage} />
            <Box p="6" bgColor={"gray.600"}>
                <Box display="flex" alignItems="baseline" mb={2} ml={-2}>
                    <Badge borderRadius="full" px="2" colorScheme="teal">
                        New Product
                    </Badge>
                </Box>

                <Box
                    mt="1"
                    fontWeight="semibold"
                    as="h4"
                    lineHeight="tight"
                    isTruncated
                    color={"white"}
                >
                    {title}
                </Box>
                <Box
                    color="white"
                    fontSize="sm"
                    display={"flex"}
                    mt={4}
                    justifyContent={"flex-end"}
                >
                    <Button
                        className="buy-button snipcart-add-item"
                        bgColor={"green.400"}
                        data-item-id={id}
                        data-item-price="5"
                        data-item-url="/"
                        data-item-name={title}
                        data-item-description={description}
                        data-item-image={cardImage}
                    >
                        Add to Basket
                    </Button>
                </Box>
            </Box>
        </Box>
    );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Одна из главных особенностей, которая отличает refine от других фреймворков, — это возможность настройки. В сочетании с refine headless он предоставляет больше возможностей для настройки. Это обеспечивает большое удобство в проекте, который вы будете разрабатывать.

Как вы можете видеть в этой статье, мы разработали клиентскую часть нашей панели администратора, что мы уже делали ранее, с помощью refine. refine предлагает возможность разрабатывать B2B и B2C приложения без каких-либо ограничений и полностью настраиваемым образом.

Обратитесь к админской части проекта →

Исходный код

Живой кодПесочница Пример

Смотрите подробную информацию о refine. →

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *