bananas-commerce-admin
Version:
What's this, an admin for apes?
186 lines • 7.59 kB
JavaScript
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