samling 0.13.1

App for managing apparel collections
Documentation
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { t } from "@lingui/macro";
import {
  Dispatch,
  FormEvent,
  Fragment,
  SetStateAction,
  useEffect,
  useState,
} from "react";
import { useNestedStyles } from "../../api";
import { useLocalize } from "../../i18n";
import { cloudflareImageUrl } from "../../images";
import { EditableStyle, makeEditableStyle, transferIsNew } from "../../types/admin";
import {
  EntityFilterChoice,
  ItemFilterChoices,
  NestedStyleSummary,
  StyleFilters,
} from "../../types/api";
import MultipleCombobox from "./MultipleCombobox";

type ItemStatus = string;

export default function AddCollectionItemsModal({
  open,
  setOpen,
  allNestedStylesMap,
  setEditableStyles,
  itemFilterChoices,
}: {
  open: boolean;
  setOpen: Dispatch<SetStateAction<boolean>>;
  allNestedStylesMap: Map<number, NestedStyleSummary>;
  setEditableStyles: Dispatch<SetStateAction<EditableStyle[]>>;
  itemFilterChoices: ItemFilterChoices;
}) {
  const { i18nDbText } = useLocalize();

  const [filters, setFilters] = useState(() => ({} as StyleFilters));
  const [nestedStylesResult] = useNestedStyles(filters);
  const nestedStyles = nestedStylesResult.unwrapOr([]) || [];
  // Status filter
  const [filteredStatuses, setFilteredStatuses] = useState(
    () => [] as ItemStatus[],
  );

  // Style filter
  const [filteredStyles, setFilteredStyles] = useState(
    () => [] as EntityFilterChoice[],
  );

  // Category filter
  const [filteredCategories, setFilteredCategories] = useState(
    () => [] as EntityFilterChoice[],
  );

  // Attribute filter
  const [filteredAttributes, setFilteredAttributes] = useState(
    () => [] as EntityFilterChoice[],
  );

  // Apply filters
  useEffect(() => {
    setFilters({
      status: filteredStatuses.length > 0 ? filteredStatuses : null,
      refs:
        filteredStyles.length > 0
          ? filteredStyles.map((style) => ({ id: style.id }))
          : null,
      categories:
        filteredCategories.length > 0
          ? filteredCategories.map((cat) => ({ id: cat.id }))
          : null,
      attributes:
        filteredAttributes.length > 0
          ? filteredAttributes.map((cat) => ({ id: cat.id }))
          : null,
    });
  }, [filteredStatuses, filteredCategories, filteredAttributes, filteredStyles, setFilters]);

  function onSubmitForm(evt: FormEvent<HTMLFormElement>) {
    evt.preventDefault();
    evt.stopPropagation();
    setOpen(false);
    setEditableStyles((prevEditableStyles) => {
      const prevMapIndexes = new Map(
        prevEditableStyles.map((style, idx) => [style.id, idx]),
      );
      const newEditableStyles = nestedStyles.flatMap((nestedStyle) => {
        const fullStyle = allNestedStylesMap.get(nestedStyle.id);
        if (fullStyle) {
          const newStyle = makeEditableStyle(nestedStyle || fullStyle, fullStyle);
          const prevIdx = prevMapIndexes.get(nestedStyle.id);
          if (prevIdx === undefined) {
            return [newStyle];
          } else {
            const prevStyle = prevEditableStyles[prevIdx];
            prevEditableStyles[prevIdx] = transferIsNew(prevStyle, newStyle);
            return [];
          }
        } else {
          throw Error(
            `Didn't find style and/or fullStyle for ${JSON.stringify(
              nestedStyle,
            )}`,
          );
        }
      });
      return [...newEditableStyles, ...prevEditableStyles];
    });
  }

  return (
    <Transition.Root show={open} as={Fragment}>
      <Dialog as="div" className="relative z-10" onClose={() => setOpen(false)}>
        <Transition.Child
          as={Fragment}
          enter="ease-out duration-300"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in duration-200"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
        </Transition.Child>

        <div className="fixed inset-0 z-10 overflow-y-auto">
          <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
            <Transition.Child
              as={Fragment}
              enter="ease-out duration-300"
              enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
              enterTo="opacity-100 translate-y-0 sm:scale-100"
              leave="ease-in duration-200"
              leaveFrom="opacity-100 translate-y-0 sm:scale-100"
              leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
            >
              <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
                <div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
                  <button
                    type="button"
                    className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
                    onClick={() => setOpen(false)}
                  >
                    <span className="sr-only">Close</span>
                    <XMarkIcon className="h-6 w-6" aria-hidden="true" />
                  </button>
                </div>
                <div>
                  <div className="px-4 py-5 sm:p-6">
                    <Dialog.Title
                      as="h3"
                      className="text-lg font-medium leading-6 text-gray-900"
                    >
                      {t`Add collection items`}
                    </Dialog.Title>
                    <div className="mt-2 max-w-xl text-sm text-gray-500">
                      <p>{t`Make your selection of items to add here. Manual adjustments can be made once added.`}</p>
                    </div>
                    <form onSubmit={onSubmitForm}>
                      <div className="my-5">
                        <MultipleCombobox
                          title={t`Status`}
                          description={t`This filter is applied at the size level which means that it will affect which colors/sizes are selected for each style.`}
                          allItems={itemFilterChoices.status}
                          selectedItems={filteredStatuses}
                          setSelectedItems={setFilteredStatuses}
                          toFilterItem={(status) => ({
                            title: status,
                          })}
                        />
                      </div>
                      <div className="my-5">
                        <MultipleCombobox
                          title={t`Categories`}
                          allItems={itemFilterChoices.category}
                          selectedItems={filteredCategories}
                          setSelectedItems={setFilteredCategories}
                          toFilterItem={(choice) => ({
                            id: choice.id,
                            title: i18nDbText(choice.title),
                            subtitle: choice.subtitle ? i18nDbText(choice.subtitle) : '',
                            imageUrl: choice.image ? cloudflareImageUrl(choice.image.url, "thumbnail") : null,
                          })}
                        />
                      </div>
                      <div className="my-5">
                        <MultipleCombobox
                          title={t`Attributes`}
                          description={t`Different types of attributes must all get matched. Within the same type any may match.`}
                          allItems={itemFilterChoices.attribute}
                          selectedItems={filteredAttributes}
                          setSelectedItems={setFilteredAttributes}
                          toFilterItem={(choice) => ({
                            id: choice.id,
                            title: i18nDbText(choice.title),
                            bubblePrefix: choice.subtitle ? i18nDbText(choice.subtitle) : '',
                            subtitle: choice.subtitle ? i18nDbText(choice.subtitle) : '',
                          })}
                          numSelectedVisible={2}
                        />
                      </div>
                      <div className="my-5">
                        <MultipleCombobox
                          title={t`Style`}
                          allItems={itemFilterChoices.style}
                          selectedItems={filteredStyles}
                          setSelectedItems={setFilteredStyles}
                          toFilterItem={(choice) => ({
                            id: choice.id,
                            title: i18nDbText(choice.title),
                            subtitle: choice.subtitle ? i18nDbText(choice.subtitle) : '',
                            imageUrl: choice.image ? cloudflareImageUrl(choice.image.url, "thumbnail") : null,
                          })}
                        />
                      </div>
                      <div>
                        <ItemStats nestedStyles={nestedStyles} />
                      </div>
                      <div className="mt-6 sm:mt-6 sm:flex sm:flex-row-reverse">
                        <button
                          type="submit"
                          className="bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500 mt-3 inline-flex w-full items-center justify-center rounded-md border border-transparent px-4 py-2 font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
                        >
                          {t`Add`}
                        </button>
                        <button
                          type="button"
                          className="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
                          onClick={() => setOpen(false)}
                        >
                          {t`Cancel`}
                        </button>
                      </div>
                    </form>
                  </div>
                </div>
              </Dialog.Panel>
            </Transition.Child>
          </div>
        </div>
      </Dialog>
    </Transition.Root>
  );
}

function ItemStats({ nestedStyles }: { nestedStyles: NestedStyleSummary[] }) {
  const numStyles = nestedStyles.length;
  const numColors = nestedStyles
    .map((style) => style.colors.length)
    .reduce((sum, count) => sum + count, 0);
  const numSizes = nestedStyles
    .flatMap((style) => style.colors.map((color) => color.sizes.length))
    .reduce((sum, count) => sum + count, 0);
  const stats = [
    { title: t`Number of styles`, stat: numStyles },
    { title: t`Number of colors`, stat: numColors },
    { title: t`Number of sizes`, stat: numSizes },
  ];
  return (
    <div>
      <h3 className="text-lg font-medium leading-6 text-gray-900">
        {t`Results from current filters`}
      </h3>
      <dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
        {stats.map((item) => (
          <div
            key={item.title}
            className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
          >
            <dt className="truncate text-sm font-medium text-gray-500">
              {item.title}
            </dt>
            <dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
              {item.stat}
            </dd>
          </div>
        ))}
      </dl>
    </div>
  );
}