Dodawanie niestandardowych atrybutów w Medusa.js (Część 2 – Interfejs użytkownika)
Autor Viktor Holik
Autor Viktor Holik
Witaj ponownie w drugiej części naszej podróży po Medusa.js! W pierwszej części dowiedzieliśmy się jak dodać wtyczkę do naszego backendu Medusa.js oraz atrybuty. Jeśli to przegapiłeś, przeczytaj poprzedni artykuł.
Od poprzedniej części opracowałem nową funkcję — atrybut zasięgu. Przejdźmy teraz do przyjemniejszej części — sprawienia, by nasza aplikacja wyglądała jeszcze lepiej.
W tym artykule skupimy się na dodaniu kilku przydatnych filtrów do interfejsu użytkownika. W tym celu użyjemy Next.js 13.5 z routingiem stron i najnowszym backendem Medusa.
Na początek ulepszymy nasz panel administracyjny, dodając dodatkowe atrybuty, takie jak „styl”, „wyprzedaż” i „wzrost”. Atrybuty te pełnią rolę znaczników pozwalających lepiej kategoryzować nasze produkty.
Po dodaniu przejdziemy do sekcji produktów, aby zastosować te znaczniki.
Po sprawdzeniu trasy /store/products za pomocą Postmana zobaczysz odpowiedź podobną do poniższej (dla przejrzystości napisałem to zwięźle):
{
"products": [
{
"id": "prod_01HER131GM72MNSC4BQ7QXNYDP",
"title": "Cap",
// attribute_values are single, boolean and multiple type attributes
"attribute_values": [
{
"id": "attr_val_01HEQS1GY28ZQ4R0P2BTJG55VG",
"value": "Streetwear",
"attribute": {
"id": "attr_01HEQS1GY2353DC4FT8XK9R8DY",
"name": "Style",
"type": "single"
}
}
],
// int_attributes_values are range attributes
"int_attribute_values": [
{
"id": "int_attr_val_01HER14ZKFG2TVET62G8F1V05G",
"value": 20,
"attribute": {
"id": "attr_01HEQV5KH63GDDN9G7HXR1XE7V",
"name": "Height"
}
}
]
},
{
"id": "prod_01HER110FNS3RTPNKG3WMGNPBS",
"title": "Hoodie",
"attribute_values": [
{
"id": "attr_val_01HEQS1GY2W21C6W4NW26VNNQB",
"value": "Vintage",
"attribute": {
"id": "attr_01HEQS1GY2353DC4FT8XK9R8DY",
"name": "Style",
"type": "single"
}
}
],
"int_attribute_values": [
{
"id": "int_attr_val_01HER159V9B0HEEFSBGNWT0X12",
"value": 70,
"attribute": {
"id": "attr_01HEQV5KH63GDDN9G7HXR1XE7V",
"name": "Height"
}
}
]
},
{
"id": "prod_01HEQV4Z3D09KZCME3Y3WECNF8",
"title": "White t-shirt",
"attribute_values": [
{
"id": "attr_val_01HEQS72F71B018W3G8J8MG340",
"value": "On sale",
"attribute": {
"id": "attr_01HEQS72F7KDQCTEVAQ0EG5GS8",
"name": "On sale",
"type": "boolean"
}
},
{
"id": "attr_val_01HEQS1GY28ZQ4R0P2BTJG55VG",
"value": "Streetwear",
"attribute": {
"id": "attr_01HEQS1GY2353DC4FT8XK9R8DY",
"name": "Style",
"type": "single"
}
}
]
}
]
}
Skupmy się teraz na skonfigurowaniu naszej aplikacji Next.js. W tym przewodniku przedłożymy funkcjonalność nad estetykę, ponieważ stylizacja została już omówiona.
Aby zoptymalizować naszą aplikację, ustalimy kontekst filtrów umożliwiający efektywne zarządzanie stanem aplikacji.
import React from "react";
interface ProductFiltersContextProps {
attributes?: Record<string, string[]> | null;
intAttributes?: Record<string, number[]> | null;
setAttributes?: React.Dispatch<React.SetStateAction<Record<string, string[]> | null>>
setIntAttributes?: React.Dispatch<
React.SetStateAction<Record<string, number[]> | null | undefined>
>;
}
const ProductFiltersContext = React.createContext<ProductFiltersContextProps>(
{}
);
interface ProductFiltersProviderProps {
initialValues: ProductFiltersContextProps;
children: React.ReactNode;
}
export const ProductFiltersProvider = ({
children,
initialValues,
}: ProductFiltersProviderProps) => {
const {
attributes: initialAttributes,
intAttributes: initialIntAttributes, // Range attributes
} = initialValues;
const [intAttributes, setIntAttributes] =
React.useState<Record<string, number[]>>(initialIntAttributes ?? {});
const [attributes, setAttributes] = React.useState<Record<
string,
string[]
> | null>(initialAttributes ?? {});
return (
<ProductFiltersContext.Provider
value={{
attributes,
setAttributes,
intAttributes,
setIntAttributes,
}}
>
{children}
</ProductFiltersContext.Provider>
);
};
export const useProductFilters = () => {
const context = React.useContext(ProductFiltersContext);
return context;
};
Oto komponent filtrów atrybutów
export const AttributeFilters = React.memo((props: ProductFiltersProps) => {
const { className } = props;
const {
setAttributes,
attributes,
intAttributes,
setIntAttributes,
} = useProductFilters();
// Custom hook to fetch "/store/attributes" route
const { attributes: customAttributes } = useAttributes();
return (
<VStack
className={classnames(cls.AttributeFilters, {}, [className])}
gap="32"
>
{customAttributes?.map((attribute) => {
if (attribute.type === "boolean") {
const checked = attributes?.[attribute.handle] ?? false;
return (
<Checkbox
key={attribute.id}
name={attribute.name}
checked={!!checked}
onChange={() => {
if (checked) {
const newAttributes = { ...attributes };
delete newAttributes[attribute.handle];
setAttributes?.(newAttributes);
} else {
setAttributes?.((prev) => ({
...prev,
[attribute.handle]: [attribute.values[0].id],
}));
}
}}
>
{attribute.name}
</Checkbox>
);
}
if (attribute.type === "range") {
return (
<Range
name={attribute.name}
maxValue={100}
minValue={0}
key={attribute.id}
setValues={(value) => {
setIntAttributes?.((prev) => ({
...prev,
[attribute.id]: value,
}));
}}
values={[
intAttributes?.[attribute.id]?.[0] ?? 0,
intAttributes?.[attribute.id]?.[1] ?? 100,
]}
/>
);
}
return (
<CheckboxList
key={attribute.id}
checked={attributes?.[attribute.handle] ?? []}
label={attribute.name}
options={attribute.values.map((value) => ({
value: value.id,
label: value.value,
}))}
onChange={(values) =>
setAttributes?.((prev) => ({
...prev,
[attribute.handle]: values,
}))
}
/>
);
})}
</VStack>
);
});
Gdy już wykorzystamy hooki atrybutów, następnym krokiem będzie wprowadzenie hooka produktów w celu pobierania odpowiedzi z backendu. Osobiście preferuję do tego celu SWR, ale możesz użyć dowolnej abstrakcji pobierania, która odpowiada Twoim preferencjom.
import { $api } from "@/shared/api";
import { Product } from "@medusajs/client-types";
import useSWR, { Fetcher } from "swr";
const fetcher: Fetcher<
{ products: Product[]; count: number },
Record<string, unknown>
> = (params) =>
$api
.get(`store/products`, {
params,
})
.then((res) => res.data);
export const PRODUCTS_KEY = `store/products`;
export const useProducts = (params: Record<string, unknown>) => {
const { data, error, isLoading } = useSWR(
[PRODUCTS_KEY, params],
([_, params]) => fetcher(params)
);
return {
products: data?.products,
count: data?.count,
error,
isLoading,
};
};
Świetnie, przejdźmy teraz do naszego komponentu listy produktów.
export const ProductList = React.memo((props: ProductListProps) => {
const { className } = props;
const filters = useProductFilters();
const representationParams = React.useMemo(
() => ({
attributes: filters.attributes ?? undefined,
int_attributes: filters.intAttributes ?? undefined,
}),
[filters]
);
const { products } = useProducts(representationParams);
return (
<div className={classnames(cls.ProductList, {}, [className])}>
{products?.map((product) => (
<ProductTile product={product} key={product.id} />
))}
</div>
);
});
Wtyczka Medusa Tolgee do obsługi wielu języków integruje platformę eCommerce Medusa z Tolgee, platformą lokalizacyjną typu open-source, oferując łatwe rozwiązanie do zarządzania tłumaczeniami...
W dzisiejszym nieustannie rozwijającym się świecie cyfrowym, gdzie użytkownicy oczekują szybkości, wydajności i bezproblemowego korzystania z aplikacji internetowych, rola Frontend Developer’a staje się niezmiernie istotna...