diff --git a/.xo-config.json b/.xo-config.json new file mode 100644 index 000000000..8536300d0 --- /dev/null +++ b/.xo-config.json @@ -0,0 +1,5 @@ +{ + "rules": { + "complexity": ["warn", { "max": 25 }] + } +} diff --git a/package-lock.json b/package-lock.json index e5dc41503..e3ad8765f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6958,9 +6958,9 @@ } }, "node_modules/@storybook/core-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -13623,9 +13623,9 @@ "dev": true }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "dev": true, "dependencies": { "bn.js": "^4.11.9", @@ -22173,12 +22173,12 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -30067,8 +30067,9 @@ } }, "node_modules/ws": { - "version": "7.5.5", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -35989,9 +35990,9 @@ } }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true } } @@ -41066,9 +41067,9 @@ "dev": true }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "dev": true, "requires": { "bn.js": "^4.11.9", @@ -47375,12 +47376,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -53453,8 +53454,9 @@ } }, "ws": { - "version": "7.5.5", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true }, "x-default-browser": { diff --git a/src/components/DatePicker/DatePicker.helpers.ts b/src/components/DatePicker/DatePicker.helpers.ts new file mode 100644 index 000000000..33dd7e23d --- /dev/null +++ b/src/components/DatePicker/DatePicker.helpers.ts @@ -0,0 +1,49 @@ +import * as de from "dayjs/locale/de"; +import * as en from "dayjs/locale/en"; +import * as enGB from "dayjs/locale/en-gb"; +import * as es from "dayjs/locale/es"; +import * as esMX from "dayjs/locale/es-mx"; +import * as fr from "dayjs/locale/fr"; +import type { DayPickerProps } from "react-day-picker"; + +export type LocaleCode = keyof typeof locales; + +interface ExtendedDayPickerProps extends DayPickerProps { + monthsShort?: string; +} + +export type LocalizationProps = Pick< + ExtendedDayPickerProps, + "locale" | "months" | "monthsShort" | "weekdaysLong" | "weekdaysShort" | "firstDayOfWeek" +>; + +const locales = { + en: en, + en_US: en, + en_GB: enGB, + + es: es, + es_ES: es, + es_MX: esMX, + + fr: fr, + de: de, +}; + +export const getLocalizationProps = async (localeCode: LocaleCode): Promise> => { + try { + const locale = await locales[localeCode]; + + return { + locale: localeCode, + weekdaysLong: locale.weekdays, + weekdaysShort: locale.weekdaysShort, + months: locale.months, + monthsShort: locale.monthsShort, + firstDayOfWeek: locale.weekStart, + }; + } catch (error) { + console.error("Error: React Day Picker localization error", error); + throw error; + } +}; diff --git a/src/components/DatePicker/DatePicker.jsx b/src/components/DatePicker/DatePicker.jsx index 1c15fe41c..4e3efc6c0 100644 --- a/src/components/DatePicker/DatePicker.jsx +++ b/src/components/DatePicker/DatePicker.jsx @@ -1,14 +1,16 @@ import clsx from "clsx"; import dayjs from "dayjs"; import PropTypes from "prop-types"; -import React, { useEffect, useMemo, useState } from "react"; -import DayPicker, { DateUtils } from "react-day-picker"; +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { DateUtils } from "react-day-picker"; import "react-day-picker/lib/style.css"; import "./DatePicker.css"; import { isArray, isFunction } from "lodash"; import { Tooltip } from "../.."; import { isSame, isValidTimeZoneName, now, toDate } from "../../helpers/date"; +import { Context } from "../Provider"; import { Day } from "./Day"; +import { LocalizedDayPicker } from "./LocalizedDayPicker"; import { MonthYearSelector } from "./MonthYearSelector"; import { NavbarElement } from "./NavbarElement"; import RangeDatePicker from "./RangeDatePicker"; @@ -38,9 +40,11 @@ export const DatePicker = ({ components = {}, getTooltip, upcomingDates, + locale, timezoneName = null, // seller timezone (e.g. "America/Los_Angeles") to return correct today date ...rest }) => { + const { locale: contextLocale } = useContext(Context); const initialValue = value ? (variant === variants.single ? value : value.from) : null; const [currentMonth, setCurrentMonth] = useState(initialValue ?? now(null, timezoneName).toDate()); const [startMonth, setStartMonth] = useState(() => { @@ -183,7 +187,14 @@ export const DatePicker = ({ // TODO: Should be outside this component because this returns JSX const CaptionElement = useMemo(() => { return shouldShowYearPicker && currentMonth - ? ({ date }) => + ? ({ date }) => ( + + ) : undefined; // Adding `handleMonthChange` causes a lot of re-renders, and closes drop-down. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -250,11 +261,12 @@ export const DatePicker = ({ handleEndMonthChange={handleEndMonthChange} handleTodayClick={handleTodayClick} selectedDays={selectedDays} + locale={locale ?? contextLocale} timezoneName={timezoneName} {...rest} /> ) : ( - { - +
diff --git a/src/components/DatePicker/LocalizedDayPicker.tsx b/src/components/DatePicker/LocalizedDayPicker.tsx new file mode 100644 index 000000000..49ea8d03a --- /dev/null +++ b/src/components/DatePicker/LocalizedDayPicker.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx"; +import React, { forwardRef, useContext, useEffect, useState } from "react"; +import DayPicker, { DayPickerProps } from "react-day-picker"; +import { Context } from "../Provider"; +import { getLocalizationProps, LocaleCode, LocalizationProps } from "./DatePicker.helpers"; + +export const LocalizedDayPicker = forwardRef(({ className, ...rest }, ref) => { + const { locale } = useContext(Context); + const [localizationProps, setLocalizationProps] = useState>({}); + console.log("Locale", locale); + + useEffect(() => { + setLocalizationProps({}); + + /** We don't want any localization-related props for "English" */ + if (!locale || locale === "en" || locale === "en_US") return; + + getLocalizationProps(locale as LocaleCode).then(setLocalizationProps); + }, [locale]); + + return ; +}); diff --git a/src/components/DatePicker/MonthYearSelector.jsx b/src/components/DatePicker/MonthYearSelector.jsx index c29c20883..5f2731668 100644 --- a/src/components/DatePicker/MonthYearSelector.jsx +++ b/src/components/DatePicker/MonthYearSelector.jsx @@ -9,11 +9,14 @@ const getDiffInMonths = (to, from) => { return 12 * (to.getFullYear() - from.getFullYear()) + (to.getMonth() - from.getMonth()); }; -export const MonthYearSelector = ({ date, onChange, currentMonth }) => { - const months = [...Array.from({ length: 12 }).keys()].map((m) => today.month(m).format("MMM")); +export const MonthYearSelector = ({ date, locale, onChange, currentMonth }) => { + const months = [...Array.from({ length: 12 }).keys()].map((m) => today.locale(locale).month(m).format("MMM")); // 2012 as baseline + 5 years in future const years = [...Array.from({ length: today.year() - 2012 + 5 + 1 }).keys()].map((y) => - today.year(2012 + y).format("YYYY"), + today + .locale(locale) + .year(2012 + y) + .format("YYYY"), ); /** diff --git a/src/components/DatePicker/RangeDatePicker.jsx b/src/components/DatePicker/RangeDatePicker.jsx index b6a367af5..46dac73dd 100644 --- a/src/components/DatePicker/RangeDatePicker.jsx +++ b/src/components/DatePicker/RangeDatePicker.jsx @@ -2,10 +2,10 @@ import clsx from "clsx"; import { isArray, isFunction } from "lodash"; import PropTypes from "prop-types"; import React from "react"; -import DayPicker from "react-day-picker"; import { now } from "../../helpers/date"; import { Tooltip } from "../Tooltip"; import { Day } from "./Day"; +import { LocalizedDayPicker } from "./LocalizedDayPicker"; import { MonthYearSelector } from "./MonthYearSelector"; import { NavbarElement } from "./NavbarElement"; @@ -24,6 +24,7 @@ const RangeDatePicker = ({ handleStartMonthChange, handleEndMonthChange, handleTodayClick, + locale, timezoneName, ...rest }) => { @@ -32,7 +33,9 @@ const RangeDatePicker = ({ const createCaptionElement = (currentMonth, handleChange) => shouldShowYearPicker && currentMonth - ? ({ date }) => + ? ({ date }) => ( + + ) : undefined; const CaptionStartElement = createCaptionElement(startMonth, handleStartMonthChange); @@ -90,7 +93,7 @@ const RangeDatePicker = ({ return (
- - {
{ +export const Provider = ({ children, localize = true, locale = "en_US" }) => { const idCounterRef = useRef(1); const generateId = useCallback(() => { return idCounterRef.current++; }, [idCounterRef]); - const value = useMemo(() => ({ generateId }), [generateId]); + const value = useMemo(() => ({ generateId, localize, locale }), [generateId, localize, locale]); return {children}; }; diff --git a/src/components/Sidebar/Sidebar.Button.jsx b/src/components/Sidebar/Sidebar.Button.jsx index a84817a6a..493e62639 100644 --- a/src/components/Sidebar/Sidebar.Button.jsx +++ b/src/components/Sidebar/Sidebar.Button.jsx @@ -1,11 +1,15 @@ +import clsx from "clsx"; import PropTypes from "prop-types"; import React from "react"; -export const SidebarButton = ({ icon: Icon, label, ...rest }) => { +export const SidebarButton = ({ icon: Icon, label, className, ...rest }) => { return ( ); @@ -34,6 +43,7 @@ export const SidebarLink = ({ isActive = false, icon: Icon, children, isSubMenuI SidebarLink.displayName = "Sidebar.Link"; SidebarLink.propTypes = { + align: PropTypes.oneOf(["center", "left", "right"]), isActive: PropTypes.bool, icon: PropTypes.func, children: PropTypes.node.isRequired, diff --git a/src/hooks/useViewportHeight.js b/src/hooks/useViewportHeight.js index a2c7b74e6..4ea31c6db 100644 --- a/src/hooks/useViewportHeight.js +++ b/src/hooks/useViewportHeight.js @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; export const useViewportHeight = () => { const [viewportHeight, setViewportHeight] = useState(0);