Dodawanie niestandardowych atrybutów w Medusa.js (Część 2 – Interfejs użytkownika)

Autor Viktor Holik

Featured image

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.

Wzmacnianie sklepu poprzez dodanie atrybutów

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.

style attributes
height attribute
on sale attribute

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.

custom attribute 2

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>
  );
});

Rezultat

custom attributes gif

Inne posty na blogu

Maintance mode w aplikacjach Next.js

Jak zaimplementować maintenance mode w Next.js? Czy jest to równie proste, co kilkuminutowa konfiguracja wtyczki w WordPress’ie? Oczywiście, że tak!

Medusa vs Magento: Całkowity koszt posiadania

Magento, w porównaniu do Medusy, może prowadzić do wyższych kosztów długoterminowych z powodu swojej licencji oraz ryzyka związanego ze stopniowym spadkiem popularności języka PHP...

Opowiedz nam o swoim projekcie

Myślisz o nowym projekcie? Zrealizujmy go!

Naciskając „Wyślij wiadomość” udzielasz nam, tj. Rigby, zgody na email marketing naszych usług w ramach komunikacji dotyczącej Twojego projektu. Zgodę możesz wycofać, np. pisząc na adres hello@rigbyjs.com.
Więcej
placeholder

Grzegorz Tomaka

Co-CEO & Co-founder

LinkedIn icon
placeholder

Jakub Zbaski

Co-CEO & Co-founder

LinkedIn icon