В этой статье мы создадим приложение для электронной коммерции из нашей админ-панели 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. →