UNPKG

bananas-commerce-admin

Version:

What's this, an admin for apes?

186 lines 7.59 kB
import React, { useCallback, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; import AbcIcon from "@mui/icons-material/Abc"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import AlternateEmailIcon from "@mui/icons-material/AlternateEmail"; import ClassIcon from "@mui/icons-material/Class"; import LanguageIcon from "@mui/icons-material/Language"; import NumbersIcon from "@mui/icons-material/Numbers"; import PhoneIcon from "@mui/icons-material/Phone"; import QuestionMarkOutlinedIcon from "@mui/icons-material/QuestionMarkOutlined"; import SearchIcon from "@mui/icons-material/Search"; import FormControl from "@mui/material/FormControl"; import IconButton from "@mui/material/IconButton"; import Stack from "@mui/material/Stack"; import { isValidPhoneNumber, parsePhoneNumberWithError } from "libphonenumber-js"; import { useSnackbar } from "notistack"; import { useI18n } from "../contexts/I18nContext"; import { isEmail } from "../util/is_email"; import { isPositiveInteger } from "../util/is_positive_integer"; import ChipsInput from "./ChipsInput"; const styles = { root: { position: "relative", ml: 2, width: "100%", maxWidth: 420, }, formControl: { justifyContent: "center", }, button: { position: "absolute", zIndex: 1, ml: 2, }, }; export function assertChipType(type) { // prettier-ignore if (!["code", "email", "name", "phone", "purchase_number", "query", "search", "site_code", "variant"].includes(type)) { throw new Error(`Invalid chip type: ${type}`); } } export const ChipIcons = { code: NumbersIcon, email: AlternateEmailIcon, name: AbcIcon, phone: PhoneIcon, purchase_number: NumbersIcon, query: QuestionMarkOutlinedIcon, search: AccountCircleIcon, site_code: LanguageIcon, variant: ClassIcon, }; export const getChipIcon = (type) => { return ChipIcons[type] ?? QuestionMarkOutlinedIcon; }; /** * Parses a string as into the {@link ChipsSearchInput} type returning undefined if it was not specified * which type or if it could not be inferred from its format. This function currently supports * parsing or infering emails, phone numbers and purchase numbers as {@link ChipsSearchInput}s. */ export const parseSearchInput = (allowedTypes, fallbackType) => (input) => { if (!input) return undefined; if (input.includes(":")) { // eslint-disable-next-line prefer-const let [type, value] = input.split(":"); type = type.toLowerCase(); assertChipType(type); if (!allowedTypes.includes(type)) { throw new Error(`Invalid type: ${type}`); } if (allowedTypes.includes("phone") && type === "phone") { if (isValidPhoneNumber(input, "SE")) { return { phone: parsePhoneNumberWithError(input, "SE").format("E.164") }; } else { throw new Error(`Invalid phone number: ${input}`); } } return { [type]: value }; } if (allowedTypes.includes("email") && isEmail(input)) { return { email: input }; } if (allowedTypes.includes("purchase_number") && input.startsWith("#")) { const purchase_number = input.slice(1); if (isPositiveInteger(purchase_number)) { return { purchase_number }; } } if (allowedTypes.includes("phone") && (input.startsWith("0") || input.startsWith("+")) && isValidPhoneNumber(input, "SE") // TODO: i18n ) { return { phone: parsePhoneNumberWithError(input, "SE").format("E.164") }; } return { [fallbackType]: input }; }; export const ChipsSearchBar = ({ allowedTypes, fallbackType = "query", placeholder, onChange, onSubmit, }) => { const { enqueueSnackbar } = useSnackbar(); const [searchParams, setSearchParams] = useSearchParams(); const { t } = useI18n(); const parseInput = useMemo(() => parseSearchInput(allowedTypes, fallbackType), [allowedTypes]); const [inputs, setInputs] = useState(() => allowedTypes .flatMap((type) => searchParams.getAll(type).map((value) => { try { assertChipType(type); return { [type.toLowerCase()]: value }; } catch (error) { console.error("[CHIPS_SERACH_BAR]", error); enqueueSnackbar(t("Invalid search query found when loading page."), { variant: "warning", }); return null; } })) .filter(Boolean)); const chips = useMemo(() => inputs .map((i) => { const [key, value] = Object.entries(i)[0]; assertChipType(key); return allowedTypes.includes(key) ? value : null; }) .filter(Boolean), [inputs]); const setSearchInputs = useCallback((inputs) => { setSearchParams((sp) => { const entries = Array.from(sp.entries()).filter(([key]) => { assertChipType(key); return !allowedTypes.includes(key); }); return [...entries, ...inputs.flatMap(Object.entries)]; }); }, [setSearchParams]); const handleChange = useCallback((inputs) => { try { const newInputs = inputs.map(parseInput).filter(Boolean); setInputs(newInputs); if (onChange != null) { onChange(newInputs); } else { setSearchInputs(newInputs); } } catch (error) { console.error("[CHIPS_SERACH_BAR]", error); enqueueSnackbar(t("Invalid search query."), { variant: "warning" }); } }, [setInputs, onChange]); const renderChip = useCallback((Component, key, props) => { try { const input = inputs.find((i) => { const value = Object.values(i)[0]; return value === props.title; }); if (input == null) return React.createElement(Component, { key: key, ...props }); const type = Object.keys(input)[0]; assertChipType(type); const Icon = getChipIcon(type); return React.createElement(Component, { key: key, ...props, icon: React.createElement(Icon, null) }); } catch (error) { console.error("[CHIPS_SERACH_BAR]", error); enqueueSnackbar(t("Invalid search query."), { variant: "warning" }); return React.createElement(Component, { key: key, ...props }); } }, [chips, enqueueSnackbar, parseInput, t]); return (React.createElement(Stack, { component: "form", sx: styles.root, onSubmit: onSubmit }, React.createElement(FormControl, { sx: styles.formControl }, React.createElement(IconButton, { "aria-label": "Search", sx: styles.button, type: "submit" }, React.createElement(SearchIcon, null)), React.createElement(ChipsInput, { placeholder: chips.length > 0 ? "" : placeholder, renderChip: renderChip, size: "small", validate: (input) => { try { parseInput(input); return true; } catch { enqueueSnackbar(t("Invalid search type."), { variant: "warning" }); return false; } }, value: chips, variant: "outlined", onChange: handleChange })))); }; //# sourceMappingURL=ChipsSearchBar.js.map