Введение
В этом руководстве вы узнаете, как создать внешний пользовательский интерфейс для Medusa с помощью Remix.
Medusa — это платформа электронной коммерции с открытым исходным кодом на Node.js, которая предоставляет вам множество функций электронной коммерции, таких как управление заказами, потоки RMA, управление клиентами и многое другое. Medusa также фокусируется на обеспечении хорошего опыта для разработчиков, позволяя вам начать работу в течение нескольких минут и имея сильное сообщество поддержки и документацию для поддержки.
Remix — это полнофункциональный веб-фреймворк, который позволяет создавать элегантные пользовательские интерфейсы с устойчивым пользовательским опытом. Он рендерит страницы на сервере, в отличие от большинства фреймворков React.
В этом руководстве мы сосредоточимся только на основах, которые включают в себя:
- Настройка макета витрины магазина
- Вывод списка товаров
- Отображение страницы одного товара с опциями
Ниже приведен снимок того, что мы будем создавать:
Вы можете найти полный проект в этом репозитории GitHub.
Предварительные условия
Эта статья предназначена для средних и продвинутых разработчиков React. Вы должны быть знакомы со следующим:
- учебник Remix Blog
- Учебник по Remix Jokes
Почему Remix
Remix — это новый фреймворк React, который быстро набирает популярность в последние пару лет. Он был создан авторами популярной библиотеки React Router.
Для электронной коммерции рекомендуется использовать серверные фреймворки, чтобы обеспечить лучшие возможности поисковой оптимизации, повышенную безопасность API и более быстрые динамические страницы для конечных пользователей. Remix обладает множеством ключевых преимуществ, включая:
- Он очень быстро отображает динамический контент, поскольку обработка контента и вызовы API сторонних разработчиков выполняются на сервере, а не на клиенте.
- Он отлично работает в медленных сетях, таких как 2G и 3G.
- Веб-сайты Remix работают, даже если в браузере отключен JavaScript
- Время создания и производительность не зависят от размера данных.
Почему именно Medusa
Безголовая архитектура Medusa облегчает создание витрины магазина на выбранном вами языке или фреймворке. Выбираете ли вы Remix, Gatsby, Next.js или любой другой фреймворк, вы можете использовать API Medusa для создания витрины магазина, обладающей всеми основными возможностями электронной коммерции.
Вы также можете подключить свой сервер Medusa к администратору Medusa, чтобы получить полную власть над своим магазином электронной коммерции. Администратор Medusa позволяет операторам магазина управлять товарами, заказами, клиентами, скидками и многим другим.
Настройка сервера Medusa
Первым шагом в этом руководстве будет настройка сервера Medusa, на котором будет осуществляться хранение данных и обработка бэкенда. Сначала установите программное обеспечение локально следующим образом:
# Install Medusa CLI
npm install -g @medusajs/medusa-cli
# Create a new Medusa project
medusa new my-medusa-store --seed
Опция --seed
добавляет фиктивные товары в ваш магазин, а также некоторые другие настройки.
Настройка Medusa Admin
Как уже упоминалось, Medusa предоставляет мощный интерфейс администратора, который вы можете использовать для управления вашим магазином, товарами, заказами и многим другим! Админку легко установить и использовать, однако она совершенно необязательна. Поэтому, если вас не интересует админка, вы можете перейти к следующему разделу.
В отдельной директории выполните следующую команду для установки админки:
git clone https://github.com/medusajs/admin medusa-admin
Это создаст новый каталог medusa-admin
. Перейдите в эту директорию и установите зависимости:
npm install
Теперь запустите сервер Medusa из каталога my-medusa-store
:
npm start
Затем запустите администратора Medusa из каталога medusa-admin
:
npm run develop
Если вы откроете localhost:7000
в браузере, вы увидите экран входа в систему. Опция --seed
, которую вы использовали ранее при создании магазина Medusa, добавляет пользователя admin с электронной почтой «admin@medusa-test.com» и паролем «supersecret».
Затем на боковой панели выберите Продукты. Вы увидите список товаров, доступных в вашем магазине, и сможете добавить новый товар с помощью кнопки Новый товар в правом верхнем углу.
Если вы нажмете кнопку Новый продукт или нажмете на существующий продукт для его редактирования, вы сможете ввести много информации о продукте. Вы также сможете добавить варианты, загрузить изображения и многое другое.
Настройка Remix + Tailwind CSS
В этом разделе вы быстро создадите проект Remix и настроите Tailwind CSS для быстрого создания пользовательского интерфейса. Пользовательский интерфейс не будет полностью отзывчивым, чтобы упростить учебник.
Вы также будете использовать JavaScript для написания кода, однако я настоятельно рекомендую использовать TypeScript и фреймворк Test-Driven Development для реальных фронтендов.
Мы можем быстро создать наш проект Remix следующим образом:
npx create-remix@latest remix-medusa-storefront
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Remix App Server
? TypeScript or JavaScript? JavaScript
? Do you want me to run `npm install`? (Y/n) Y
После установки перейдите в папку проекта через терминал и убедитесь, что все работает, выполнив команду npm run dev
. Убедитесь, что localhost:3000 загружается правильно. Если все загружается нормально, убейте сервер dev, прежде чем переходить к следующему шагу.
Далее с помощью официального руководства по интеграции Tailwind CSS Remix установите Tailwind CSS в ваш проект remix-medusa-storefront
следующим образом:
Шаг 1: Установите зависимости пакета
# Install Dev packages
npm install -D tailwindcss postcss autoprefixer concurrently
# Generate `tailwind.config.js` file
npx tailwindcss init -p
Шаг 2: Обновите поле content
в tailwind.config.js
, чтобы настроить файлы, используемые для процесса очистки Tailwind CSS.
module.exports = {
content: ["./app/**/*.{js,jsx}"],
theme: {
extend: {},
},
plugins: [],
};
Шаг 3: Измените скрипты dev
и build
в package.json
, чтобы добавить шаги компиляции CSS:
"scripts": {
...,
"build": "npm run build:css && remix build",
"build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
"dev": "concurrently "npm run dev:css" "remix dev"",
"dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css"
},
Шаг 4: Создайте файл ./styles/app.css
в корне проекта со следующим содержимым:
@tailwind base;
@tailwind components;
@tailwind utilities;
Шаг 5: Добавьте этот код в app/root.jsx
, чтобы разрешить загрузку скомпилированного Tailwind’ом CSS на страницы:
import styles from "~/styles/app.css";
export function links() {
return [{ rel: "stylesheet", href: styles }];
}
Шаг 6: Протестируйте настройку CSS Tailwind, заменив код в app/routes/index.jsx
следующим кодом:
export default function Index() {
return (
<div className="container mx-auto mt-8">
<h1 className="text-3xl font-bold text-gray-700 underline">
Hello world!
</h1>
</div>
);
}
Выполните npm run dev
и убедитесь, что стили Tailwind CSS загружаются на индексной странице на localhost:3000/.
Обратите внимание, что когда вы запускаете свой проект, будь то в режиме dev
или build
, файл /app/styles/app.css
генерируется для вас на основе исходных данных ./styles/app.css
. Следовательно, вы не должны трогать сгенерированный файл при внесении изменений в CSS.
Добавление /app/styles/app.css
в .gitignore
является хорошей идеей, поскольку этот файл будет сгенерирован на этапе развертывания.
Макет сайта
Теперь, когда вы успешно интегрировали Tailwind CSS в рабочий проект Remix, вы можете приступить к настройке базового макета витрины. Создайте папку app/layouts
и создайте в ней следующие файлы:
footer.jsx
navbar.jsx
index.jsx
В app/layouts/footer.jsx
добавьте следующий код:
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<div className="py-4 text-sm text-center text-gray-200 bg-gray-800">
© Copyright {currentYear} [Brand name]. All Rights Reserved
</div>
);
}
Этот фрагмент просто отображает информацию об авторских правах в текущем году.
Для Navbar необходимо отобразить:
- Логотип
- Навигационные ссылки
- значок корзины
Для логотипа вы можете включить свой собственный логотип или пока скопировать этот. Переименуйте файл в logo.svg
и поместите его в каталог /public
.
Для навигационных ссылок вы будете использовать [<NavLink>](https://remix.run/docs/en/v1/api/remix#navlink)
, который является специальным видом <Link>
, который знает, является ли страница текущей загруженной страницей. Вам потребуется стилизация для CSS-класса .active
, чтобы обеспечить визуальную индикацию.
Для иконки корзины вы просто импортируете ее из пакета React Icons. Установите его следующим образом:
npm install react-icons
Теперь, когда все необходимые ресурсы установлены, вы можете вставить следующий код в app/layouts/navbar.jsx
.
import { Link, NavLink } from "@remix-run/react";
import { BiShoppingBag } from "react-icons/bi";
export default function Navbar() {
const links = [
{
label: "Home",
url: "/",
},
{
label: "Products",
url: "/products",
},
{
label: "About",
url: "/about",
},
];
return (
<nav className="flex items-center justify-between px-8 pt-2">
{/* Site Logo */}
<div className="font-mono text-3xl font-extrabold uppercase">
<Link to="/">
<img className="w-28" src="/logo.svg" alt="Medusa" />
</Link>
</div>
{/* Navigation Links */}
<div className="space-x-4">
{links.map((link, index) => (
<NavLink key={index} to={link.url} className="navlink">
{link.label}
</NavLink>
))}
</div>
{/* Shopping Cart Indicator/Checkout Link */}
<div className="font-semibold text-gray-600 hover:text-emerald-500">
<NavLink
to="/checkout"
className="inline-flex items-center space-x-1 transition-colors duration-300"
>
<BiShoppingBag className="text-xl" /> <span>0</span>
</NavLink>
</div>
</nav>
);
}
Далее вставьте следующий код в app/layouts/index.jsx
, который будет основным макетом вашего сайта:
import Footer from "./footer";
import Navbar from "./navbar";
export default function Layout({ children }) {
return (
<>
<header className="border-b">
<Navbar />
</header>
<main className="container flex justify-center flex-grow mx-auto">
{children}
</main>
<Footer />
</>
);
}
Добавьте этот код в ./styles/app.css
после базовых стилей Tailwind, чтобы включить ваши пользовательские стили макета и навигации:
/*
Layout styling
*/
html {
@apply antialiased font-sans text-gray-800 bg-gray-200;
}
body {
@apply flex flex-col min-h-screen overflow-x-hidden;
}
/*
Typography styling
*/
h1 {
@apply text-3xl font-bold;
}
h2 {
@apply text-xl;
}
p {
@apply text-gray-700;
}
/*
Navigation menu styling
*/
.navlink {
@apply inline-block w-20 py-2 font-semibold text-center text-gray-500 hover:text-emerald-500;
}
.navlink:after {
@apply block pb-2 border-b-2 border-emerald-400 transition ease-in-out duration-300 origin-[0%_50%] content-[""] scale-x-0;
}
.navlink:hover:after {
@apply scale-x-100;
}
a.active {
@apply font-bold text-gray-700;
}
Наконец, замените весь код в app/root.jsx
, который включает ваш новый макет сайта:
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import Layout from "./layouts";
import styles from "~/styles/app.css";
export function links() {
return [{ rel: "stylesheet", href: styles }];
}
export function meta() {
return {
charset: "utf-8",
title: "Medusa Remix StoreFront",
viewport: "width=device-width,initial-scale=1",
};
}
export default function App() {
return (
<Document>
<Layout>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</Layout>
</Document>
);
}
function Document({ children }) {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>{children}</body>
</html>
);
}
export function ErrorBoundary({ error }) {
return (
<Document>
<Layout>
<div className="text-red-500">
<h1>Error</h1>
<p>{error.message}</p>
</div>
</Layout>
</Document>
);
}
Теперь вы можете снова запустить сервер dev, выполнив команду npm run dev
. Теперь ваша индексная страница localhost:3000
должна выглядеть так, как показано на скриншоте ниже:
Маршруты страницы
Теперь вы добавите страницы продуктов, информации и оформления заказа. Создайте следующие файлы в папке app/routes
:
products/index.jsx
about.jsx
checkout.jsx
Вы не будете реализовывать никакой логики для этого раздела. Вы просто разместите код, начиная с app/routes/products/index.jsx
:
export default function ProductsIndexRoute() {
return (
<div className="w-full mt-8">
<h1>Products Page</h1>
<p>List of products</p>
</div>
);
}
Скопируйте следующий финальный код для app/routes/about.jsx
:
export default function AboutRoute() {
return (
<div className="w-full mt-8">
<h1>About</h1>
<p className="mt-4 text-justify">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Labore aperiam
maxime assumenda dolore excepturi ipsam accusantium repudiandae ducimus
eum, voluptatibus, adipisci nam temporibus vel ex! Non iure dolore at
mollitia.
</p>
</div>
);
}
Скопируйте следующий код заполнителя для app/routes/checkout.jsx
:
export default function CheckoutRoute() {
return (
<div className="w-full mt-8">
<h1>Checkout Page</h1>
</div>
);
}
Завершите главную страницу, реализовав простой баннер Hero, вдохновленный TailwindUI. Замените весь код в app/routes/index.jsx
на следующий:
import { Link } from "@remix-run/react";
export default function IndexRoute() {
return (
<div>
{/* Hero Banner */}
<div className="px-12 py-32 text-center text-gray-200 bg-gray-800">
<h1 className="text-5xl text-gray-100">New arrivals are here</h1>
<p className="px-8 mt-2 font-semibold text-gray-300">
The new arrivals have, well, newly arrived. Check out the latest
options from our summer small-batch release while they're still in
stock.
</p>
<Link
to="/products"
className="inline-block px-6 py-2 mt-8 text-sm font-semibold text-gray-700 transition duration-300 bg-gray-100 rounded-md hover:bg-white hover:text-gray-900 hover:scale-110 color"
>
Shop New Arrivals
</Link>
</div>
</div>
);
}
Ваша домашняя страница должна выглядеть так, как показано на скриншоте ниже:
Перейдите и проверьте все страницы, чтобы убедиться, что код-заполнитель работает правильно. В следующем разделе вы начнете реализовывать логику для маршрута /products
.
Страница продуктов
В этом разделе вы реализуете страницу «Товары» с помощью извлечения данных с сервера Medusa и CSS-сетки.
Во-первых, убедитесь, что ваш сервер Medusa Store работает по адресу localhost:9000
. Если это не так, вы можете перейти в папку проекта Medusa в терминале и выполнить команду npm start
. Когда она будет запущена, вы можете перейти к следующему шагу.
Вернувшись к проекту remix-medusa-storefront
, установите пакет Medusa JS Client, чтобы обеспечить легкий доступ к API Medusa:
npm install @medusajs/medusa-js
Далее необходимо создать утилиту, которая поможет вам создать экземпляр клиента medusa-js
и получить к нему доступ. Создайте файл app/utils/client.js
со следующим кодом:
import Medusa from "@medusajs/medusa-js";
const BACKEND_URL = process.env.PUBLIC_MEDUSA_URL || "http://localhost:9000";
export const createClient = () => new Medusa({ baseUrl: BACKEND_URL });
Затем откройте файл apps/routes/products/index.js
и замените его на следующий:
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createClient } from "~/utils/client";
export const loader = async () => {
const client = createClient();
const { products } = await client.products.list();
return json(products);
};
export default function ProductsIndexRoute() {
const products = useLoaderData();
return (
<div className="w-full mt-8">
<h1>Latest Arrivals</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
</div>
);
}
В приведенном выше коде вы используете функцию загрузки данных Remix для запроса данных с сервера Medusa. Эти данные передаются в функцию рендеринга через хук useLoaderData
. Проверьте API продукта Medusa и посмотрите, как выглядит структура JSON. На странице /products
вы должны ожидать следующего вывода:
Теперь, когда у вас есть данные, вы можете приступить к созданию пользовательского интерфейса с использованием CSS-сеток и карточек товаров. Но сначала нам нужно создать небольшой помощник, который будет отображать цену товара.
В Medusa продукт содержит несколько вариантов, и каждый вариант имеет разные цены для нескольких валют.
Данные, которые вы загрузили ранее при создании сервера Medusa, содержат цены в долларах США и евро для каждого варианта товара. Поскольку это вводный учебник, цель которого — быть простым, вы не сможете полностью реализовать всю необходимую логику для производственного приложения, которая включает в себя:
- автоматическое определение региона и валюты пользователя с помощью геолокации
- Предоставление пользователям опций для выбора региона и валюты
- Отображение цен на товары в зависимости от выбранного региона/валюты.
Создайте файл app/utils/prices.js
и скопируйте следующий упрощенный код:
// TODO: Detect user language
const locale = "en-US";
// TODO: Detect user currency/Allow currency selection (usd | eur)
const regionCurrency = "usd";
export function formatPrice(variant) {
const price = variant.prices.find(
(price) => price.currency_code == regionCurrency
);
return new Intl.NumberFormat(locale, {
style: "currency",
currency: regionCurrency,
}).format(price.amount / 100);
}
В приведенном выше коде вместо настраиваемых переменных используются жестко закодированные константы. Функция formatPrice
принимает в качестве входных данных вариант продукта и возвращает цену в виде отформатированной строковой валюты.
Далее необходимо создать компонент ProductCard
, который будет отображать:
- Эскиз
- Название
- Цена (для 1-го варианта)
Создайте файл app/components/product-card.jsx
и скопируйте следующий код:
import { Link } from "@remix-run/react";
import { formatPrice } from "~/utils/prices";
export default function ProductCard({ product }) {
const variant = product.variants[0];
return (
<section className="overflow-hidden bg-white rounded-lg shadow:md hover:shadow-lg w-80">
<Link to={`/products/${product.id}`}>
<img className="w-80" src={product.thumbnail} alt={product.title} />
<div className="p-4">
<h3 className="text-lg font-bold text-gray-700 hover:underline">
{product.title}
</h3>
<p className="font-semibold text-teal-600">{formatPrice(variant)}</p>
</div>
</Link>
</section>
);
}
Наконец, обновите код в apps/routes/products/index.js
следующим образом:
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import ProductCard from "~/components/product-card";
import { createClient } from "~/utils/client";
export const loader = async () => {
const client = createClient();
const { products } = await client.products.list();
return json(products);
};
export default function ProductsIndexRoute() {
const products = useLoaderData();
return (
<div className="w-full p-4 my-8">
<h1 className="text-center">Latest Arrivals</h1>
<div className="grid grid-cols-1 gap-6 px-4 mt-8 md:px-12 lg:px-6 xl:px-4 xl:gap-6 2xl:px-24 2xl:gap-6 justify-items-center md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
Внедрение этих обновлений должно дать следующий результат:
Страница одного продукта
Чтобы создать страницу одного продукта, необходимо использовать соглашение об именовании файлов slug. Создайте файл apps/routes/product/$productId.jsx
со следующим содержимым:
import { useState } from "react";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { BiShoppingBag } from "react-icons/bi";
import { createClient } from "~/utils/client";
import { formatPrice } from "~/utils/prices";
export const loader = async ({ params }) => {
const client = createClient();
const { product } = await client.products.retrieve(params.productId);
return json(product);
};
export default function ProductRoute() {
const product = useLoaderData();
const [variant, setVariant] = useState(product.variants[0]);
const [image, setImage] = useState(product.images[0]);
const [quantity, setQuantity] = useState(1);
const handleVariantChange = (index) => {
setVariant(product.variants[index]);
setQuantity(1);
};
const handleQuantityChange = (action) => {
switch (action) {
case "inc":
if (quantity < variant.inventory_quantity)
setQuantity(quantity + 1);
break;
case "dec":
if (quantity > 1) setQuantity(quantity - 1);
break;
default:
break;
}
};
const handleImageChange = (id) => {
setImage(product.images.find((img) => img.id === id));
};
return (
<div className="w-full">
<div className="grid items-center md:grid-cols-2">
<div>
<img
className="w-full rounded-lg"
src={image.url}
alt={product.title}
/>
<div className="flex justify-center p-4 space-x-2">
{product.images.map((imageItem) => (
<img
className={`w-16 border-2 rounded-lg ${
imageItem.id === image.id ? "border-teal-400" : null
}`}
key={imageItem.id}
src={imageItem.url}
alt={product.title}
onClick={() => handleImageChange(imageItem.id)}
/>
))}
</div>
</div>
<div className="flex flex-col px-16 py-4 space-y-8">
<h1>{product.title} </h1>
<p className="font-semibold text-teal-600">{formatPrice(variant)}</p>
<div>
<p className="font-semibold">Select Size</p>
<div className="grid grid-cols-3 gap-2 mt-2 md:grid-cols-2 xl:grid-cols-4">
{product.variants.map((variantItem, index) => (
<button
key={variantItem.id}
className={`px-2 py-1 mr-2 text-sm hover:brightness-90 ${
variantItem.id === variant.id
? "bg-gray-700 text-gray-100"
: "bg-gray-300 text-gray-700"
}`}
onClick={() => handleVariantChange(index)}
>
{variantItem.title}
</button>
))}
</div>
</div>
<div>
<p className="font-semibold">Select Quantity</p>
<div className="flex items-center px-4 mt-2 space-x-4">
<button
className="px-4 py-2 hover:shadow-sm hover:text-teal-500 hover:font-bold"
onClick={() => handleQuantityChange("dec")}
>
-
</button>
<span>{quantity}</span>
<button
className="px-4 py-2 hover:shadow-sm hover:text-teal-500 hover:font-bold"
onClick={() => handleQuantityChange("inc")}
>
+
</button>
</div>
</div>
<div>
<button className="inline-flex items-center px-4 py-2 font-semibold text-gray-200 bg-gray-700 rounded hover:text-white hover:bg-gray-900">
<BiShoppingBag className="mr-2 text-lg" />{" "}
<span>Add to Cart</span>
</button>
</div>
<div>
<p className="font-semibold">Product Description</p>
<hr className="w-2/3 mt-2 border-t-2 border-gray-300" />
<p className="mt-4 text-gray-700">{product.description}</p>
</div>
</div>
</div>
</div>
);
}
Давайте разобьем логику на несколько этапов. Сначала вы загружаете один продукт, используя параметр маршрута productId
.
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { createClient } from "~/utils/client";
import { formatPrice } from "~/utils/prices";
export const loader = async ({ params }) => {
const client = createClient();
const { product } = await client.products.retrieve(params.productId);
return json(product);
};
export default function ProductRoute() {
const product = useLoaderData();
return (
<div className="w-full mt-8">
<h1>{product.title}</h1>
<p>{formatPrice(variant)}</p>
<p>{product.description}</p>
</div>
);
}
Вы должны быть знакомы с этим кодом, поскольку он очень похож на app/components/product-card.jsx
. Основное отличие заключается в том, что вы используете client.products.retrieve(id)
Medusa для получения данных по одному товару.
Во-вторых, вам нужно дать клиентам возможность выбирать варианты товара, которыми в данном случае являются размеры. Вы можете реализовать эту возможность с помощью хука useState
React:
import { useState } from "react";
export default function ProductRoute() {
const product = useLoaderData();
const [variant, setVariant] = useState(product.variants[0]);
const handleVariantChange = (index) => {
setVariant(product.variants[index]);
setQuantity(1);
};
return (
<div>
...
<div>
{product.variants.map((variantItem, index) => (
<button
key={variantItem.id}
onClick={() => handleVariantChange(index)}
>
{variantItem.title}
</button>
))}
</div>
</div>
)
}
Приведенный выше код создаст серию кликабельных кнопок, соответствующих каждому варианту, который есть у товара.
В-третьих, вам нужно дать конечным пользователям возможность просматривать различные изображения продукта. Вот логика для этой функции:
import { useState } from "react";
export default function ProductRoute() {
...
const [image, setImage] = useState(product.images[0]);
const handleImageChange = (id) => {
setImage(product.images.find((img) => img.id === id));
};
return (
<div>
...
<div>
<img src={image.url} alt={product.title}
/>
<div>
{product.images.map((imageItem) => (
<img
className={`w-16 border-2 rounded-lg ${
imageItem.id === image.id ? "border-teal-400" : null
}`}
key={imageItem.id}
src={imageItem.url}
alt={product.title}
onClick={() => handleImageChange(imageItem.id)}
/>
))}
</div>
</div>
</div>
)
}
В приведенном выше фрагменте под основным изображением продукта будет отображаться список кликабельных миниатюр, на которые пользователи могут нажимать для переключения и просмотра различных изображений продукта.
В-четвертых, вам нужно предоставить конечным пользователям ввод quantity
. Вам необходимо проверить этот ввод, чтобы убедиться, что:
- Количество не меньше 0
- Количество не больше, чем запасы варианта.
Вот логика для ввода количества:
import { useState } from "react";
export default function ProductRoute() {
...
const [quantity, setQuantity] = useState(1);
const handleQuantityChange = (action) => {
switch (action) {
case "inc":
if (quantity < variant.inventory_quantity) setQuantity(quantity + 1);
break;
case "dec":
if (quantity > 1) setQuantity(quantity - 1);
break;
default:
break;
}
};
return (
<div>
...
<div>
<p>Select Quantity</p>
<div>
<button onClick={() => handleQuantityChange("dec")}>
-
</button>
<span>{quantity}</span>
<button onClick={() => handleQuantityChange("inc")}>
+
</button>
</div>
</div>
</div>
)
}
Кнопки +
и -
позволят пользователям увеличивать или уменьшать желаемое количество определенного варианта. Функция handleQuantityChange
выполняет проверку для этого ввода.
Теперь, когда вы разобрались с различными логическими секциями страницы Single Product, давайте посмотрим, как выглядит готовая страница в браузере:
Вы должны иметь возможность выбирать варианты (размер), эскизы и задавать количество. Убедитесь, что все страницы товаров загружаются без ошибок.
Что дальше
Есть и другие важные функции электронной коммерции, которые вам еще предстоит реализовать. К ним относятся оформление заказа, оплата, доставка, учетные записи клиентов и другие функции.
Вы можете ознакомиться с документацией Medusa для получения более подробной информации о дальнейших действиях, в том числе:
- Как добавлять плагины. Вы также можете ознакомиться со списком плагинов, доступных в Medusa:
- Добавление методов оплаты, таких как Stripe.
- Добавление пользовательских методов доставки.
- Добавьте поиск товаров с помощью Algolia.
Если вас интересует витрина со всеми готовыми функциями электронной коммерции, Medusa предлагает витрины Next.js и Gatsby, которые вы можете использовать. Эти витрины включают такие функции, как учетные записи клиентов, списки товаров, управление корзиной и полный рабочий процесс оформления заказа.
Если у вас возникнут какие-либо проблемы или вопросы, связанные с Medusa, обращайтесь к команде Medusa через Discord. Вы также можете обратиться за поддержкой к команде Remix через Discord.