diff --git a/package.json b/package.json index ef6bc8de73..0967cbbd52 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "react-collapse": "^5.0.1", "react-collapsible": "^2.8.3", "react-country-flag": "^2.3.0", + "react-currency-input-field": "^3.6.4", "react-datepicker": "^3.6.0", "react-dom": "^16.12.0", "react-dropzone": "^11.3.4", diff --git a/src/components/molecules/shipping-option/shipping-option.stories.tsx b/src/components/molecules/shipping-option/shipping-option.stories.tsx index 1d548945da..acb887f2fb 100644 --- a/src/components/molecules/shipping-option/shipping-option.stories.tsx +++ b/src/components/molecules/shipping-option/shipping-option.stories.tsx @@ -3,7 +3,7 @@ import React from "react" import ShippingOption from "." export default { - title: "Molecules/ShippingOption", + title: "Atoms/ShippingOption", component: ShippingOption, } as ComponentMeta diff --git a/src/components/organisms/currency-input/currency-input.stories.tsx b/src/components/organisms/currency-input/currency-input.stories.tsx new file mode 100644 index 0000000000..764a406e68 --- /dev/null +++ b/src/components/organisms/currency-input/currency-input.stories.tsx @@ -0,0 +1,43 @@ +import { ComponentMeta, ComponentStory } from "@storybook/react" +import React from "react" +import CurrencyInput from "." + +export default { + title: "Organisms/CurrencyInput", + component: CurrencyInput, +} as ComponentMeta + +const Template: ComponentStory = (args) => ( + +) + +export const Default = Template.bind({}) +Default.args = { + currentCurrency: "usd", + currencyCodes: ["usd", "eur", "gbp"], +} + +export const ReadOnly = Template.bind({}) +ReadOnly.args = { + currentCurrency: "usd", + readOnly: true, +} + +const TemplateWithAmount = (args) => ( + + + +) + +export const WithAmount = TemplateWithAmount.bind({}) +WithAmount.args = { + currencyArgs: { + currentCurrency: "usd", + currencyCodes: ["usd", "eur", "krw"], + size: "small", + }, + amountArgs: { + label: "Price", + amount: 10000, + }, +} diff --git a/src/components/organisms/currency-input/index.tsx b/src/components/organisms/currency-input/index.tsx new file mode 100644 index 0000000000..a8df6cb5b0 --- /dev/null +++ b/src/components/organisms/currency-input/index.tsx @@ -0,0 +1,228 @@ +import clsx from "clsx" +import React, { useContext, useEffect, useRef, useState } from "react" +import AmountField from "react-currency-input-field" +import { Option } from "../../../types/shared" +import { currencies, CurrencyType } from "../../../utils/currencies" +import { getDecimalDigits, normalizeAmount } from "../../../utils/prices" +import MinusIcon from "../../fundamentals/icons/minus-icon" +import PlusIcon from "../../fundamentals/icons/plus-icon" +import InputContainer from "../../fundamentals/input-container" +import InputHeader from "../../fundamentals/input-header" +import Input from "../../molecules/input" +import Select from "../../molecules/select" + +type CurrencyInputProps = { + currencyCodes?: string[] + currentCurrency?: string + size?: "small" | "medium" | "full" + readOnly?: boolean + onChange?: (currencyCode: string) => void + className?: React.HTMLAttributes["className"] +} + +type CurrencyInputState = { + currencyInfo: CurrencyType | undefined +} + +type AmountInputProps = { + label: string + amount: number | undefined + required?: boolean + step?: number + allowNegative?: boolean + onChange?: (amount: number | undefined) => void +} + +const CurrencyContext = React.createContext({ + currencyInfo: undefined, +}) + +const getCurrencyInfo = (currencyCode?: string) => { + if (!currencyCode) { + return undefined + } + const currencyInfo = currencies[currencyCode.toUpperCase()] + return currencyInfo +} + +const CurrencyInput: React.FC & { + AmountInput: React.FC +} = ({ + currentCurrency, + currencyCodes, + size = "full", + readOnly = false, + onChange, + children, + className, +}) => { + const options: Option[] = + currencyCodes?.map((code) => ({ + label: code.toUpperCase(), + value: code, + })) ?? [] + + const [selectedCurrency, setSelectedCurrency] = useState< + CurrencyType | undefined + >(getCurrencyInfo(currentCurrency)) + + const [value, setValue] = useState
-

Requirements

-
-
- - -
-
- - -
-
+ {!shippingOption.is_return && ( + <> +

Requirements

+
+ + + + + + +
+ + )}

Danger Zone

diff --git a/src/domain/settings/regions/index.tsx b/src/domain/settings/regions/index.tsx index 4a7fd2540a..c0b807e57a 100644 --- a/src/domain/settings/regions/index.tsx +++ b/src/domain/settings/regions/index.tsx @@ -13,13 +13,15 @@ import NewRegion from "./new" const Regions = () => { const { regions, isLoading, refetch } = useAdminRegions() - const [selectedRegion, setSelectedRegion] = useState(null) + const [selectedRegion, setSelectedRegion] = useState( + undefined + ) const [addRegion, setAddRegion] = useState(false) useEffect(() => { const setRegion = () => { if (!isLoading && selectedRegion === null) { - setSelectedRegion(regions[0].id) + setSelectedRegion(regions?.[0]?.id) } } @@ -28,9 +30,12 @@ const Regions = () => { const handleDelete = () => { refetch().then(({ data }) => { - const id = data.regions[0].id + const id = data?.regions?.[0]?.id + + if (!id) return + setSelectedRegion(id) - document.getElementById(id).scrollIntoView({ + document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start", inline: "nearest", @@ -41,7 +46,7 @@ const Regions = () => { const handleSelect = (id: string) => { refetch().then(() => { setSelectedRegion(id) - document.getElementById(id).scrollIntoView({ + document.getElementById(id)?.scrollIntoView({ behavior: "smooth", }) }) @@ -67,7 +72,7 @@ const Regions = () => { }, ]} > - {isLoading || !selectedRegion ? ( + {isLoading || !regions ? (

@@ -76,14 +81,14 @@ const Regions = () => { value={selectedRegion} onValueChange={setSelectedRegion} > - {regions.map(r => { + {regions.map((r) => { const providers = `Payment providers: ${ r.payment_providers - .map(pp => paymentProvidersMapper(pp.id).label) + .map((pp) => paymentProvidersMapper(pp.id).label) .join(", ") || "not configured" } - Fulfillment providers: ${ r.fulfillment_providers - .map(fp => fulfillmentProvidersMapper(fp.id).label) + .map((fp) => fulfillmentProvidersMapper(fp.id).label) .join(", ") || "not configured" }` return ( @@ -92,9 +97,9 @@ const Regions = () => { sublabel={ r.countries.length ? `(${r.countries - .map(c => c.display_name) + .map((c) => c.display_name) .join(", ")})` - : null + : undefined } description={providers} value={r.id} diff --git a/src/domain/settings/regions/new-shipping.tsx b/src/domain/settings/regions/new-shipping.tsx index ceb1458e10..bc1ec3e7c0 100644 --- a/src/domain/settings/regions/new-shipping.tsx +++ b/src/domain/settings/regions/new-shipping.tsx @@ -9,7 +9,9 @@ import Button from "../../../components/fundamentals/button" import Input from "../../../components/molecules/input" import Modal from "../../../components/molecules/modal" import Select from "../../../components/molecules/select" +import CurrencyInput from "../../../components/organisms/currency-input" import useToaster from "../../../hooks/use-toaster" +import { Option } from "../../../types/shared" import { getErrorMessage } from "../../../utils/error-messages" import fulfillmentProvidersMapper from "../../../utils/fulfillment-providers.mapper" @@ -20,7 +22,7 @@ const NewShipping = ({ onCreated, onClick, }) => { - const { register, setValue, handleSubmit, errors } = useForm() + const { register, setValue, handleSubmit } = useForm() const { shipping_profiles, isLoading: isProfilesLoading, @@ -28,11 +30,21 @@ const NewShipping = ({ const [adminOnly, setAdminOnly] = useState(false) const [options, setOptions] = useState([]) const [selectedOption, setSelectedOption] = useState(null) - const [profileOptions, setProfileOptions] = useState([]) + const [profileOptions, setProfileOptions] = useState([]) const [selectedProfile, setSelectedProfile] = useState(null) const createShippingOption = useAdminCreateShippingOption() const toaster = useToaster() + useEffect(() => { + register("amount", { required: true }) + register("requirements.max_subtotal.amount") + register("requirements.min_subtotal.amount") + }, []) + + const handleAmountChange = (fieldName: string, amount?: number) => { + setValue(fieldName, amount) + } + const handleSave = (data: { name: string requirements: { amount: number; type: string }[] @@ -40,9 +52,9 @@ const NewShipping = ({ profile_id: { value: string; label: string } amount: number }) => { - const fOptions = fulfillmentOptions.map(provider => { + const fOptions = fulfillmentOptions.map((provider) => { const filtered = provider.options.filter( - o => !!o.is_return === !!isReturn + (o) => !!o.is_return === !!isReturn ) return { @@ -60,7 +72,7 @@ const NewShipping = ({ if (data.requirements) { reqs = Object.entries(data.requirements).reduce((acc, [key, value]) => { if (value.amount && value.amount > 0) { - acc.push({ type: key, amount: Math.round(value.amount * 100) }) + acc.push({ type: key, amount: value.amount }) return acc } else { return acc @@ -75,7 +87,7 @@ const NewShipping = ({ profile_id: data.profile_id?.value, requirements: reqs, price_type: "flat_rate", - amount: Math.round(data.amount * 100), + amount: data.amount, is_return: isReturn, provider_id, admin_only: adminOnly, @@ -89,7 +101,7 @@ const NewShipping = ({ } onClick() }, - onError: error => { + onError: (error) => { toaster(getErrorMessage(error), "error") }, }) @@ -98,7 +110,7 @@ const NewShipping = ({ useEffect(() => { const opts = fulfillmentOptions.reduce((acc, provider, p) => { const filtered = provider.options.filter( - o => !!o.is_return === !!isReturn + (o) => !!o.is_return === !!isReturn ) return acc.concat( @@ -117,9 +129,9 @@ const NewShipping = ({ }, [fulfillmentOptions]) useEffect(() => { - const opts = isProfilesLoading + const opts = !shipping_profiles ? [] - : shipping_profiles.map(p => ({ + : shipping_profiles.map((p) => ({ label: p.name, value: p.id, })) @@ -131,12 +143,12 @@ const NewShipping = ({ } }, [isProfilesLoading, shipping_profiles]) - const handleProfileChange = value => { + const handleProfileChange = (value) => { setValue("profile_id", value) setSelectedProfile(value) } - const handleFulfillmentChange = value => { + const handleFulfillmentChange = (value) => { setValue("fulfillment_option", value) setSelectedOption(value) } @@ -160,25 +172,13 @@ const NewShipping = ({ placeholder="New Shipping Option" className="flex-grow" /> -
- - + handleAmountChange("amount", v)} + amount={undefined} /> -
+
- {loadingOptions ? ( + {!shipping_options ? ( { ) : ( shipping_options - .filter(o => o.is_return === false && o.region_id === region.id) - .map(option => { + .filter((o) => o.is_return === false && o.region_id === region.id) + .map((option) => { return (
{ ) : shipping_options ? ( shipping_options - .filter(o => o.is_return && o.region_id === region.id) - .map(option => { + .filter((o) => o.is_return && o.region_id === region.id) + .map((option) => { return (