diff --git a/packages/lake/__stories__/DatePicker.stories.tsx b/packages/lake/__stories__/DatePicker.stories.tsx index ffe046845..fede209b8 100644 --- a/packages/lake/__stories__/DatePicker.stories.tsx +++ b/packages/lake/__stories__/DatePicker.stories.tsx @@ -10,6 +10,10 @@ import { isTodayOrFutureDate, validateDateRangeOrder, } from "@swan-io/shared-business/src/components/DatePicker"; +import { InlineDatePicker } from "@swan-io/shared-business/src/components/InlineDatePicker"; +import { extractDate } from "@swan-io/shared-business/src/utils/date"; +import { validateBirthdate } from "@swan-io/shared-business/src/utils/validation"; +import { useForm } from "@swan-io/use-form"; import { useRef, useState } from "react"; import { StyleSheet, View } from "react-native"; import { Except } from "type-fest"; @@ -198,3 +202,40 @@ export const ButtonWithRangePopover = () => { ); }; + +export const Inline = () => { + const initialValue = extractDate("1990-12-19"); + + const { Field } = useForm({ + birthDate: { + initialValue: { + day: initialValue?.day ?? "", + month: initialValue?.month ?? "", + year: initialValue?.year ?? "", + }, + validate: validateBirthdate, + }, + }); + return ( + + + + + {({ value, error, onChange, onBlur }) => ( + + + + )} + + + + + ); +}; diff --git a/packages/shared-business/src/components/InlineDatePicker.tsx b/packages/shared-business/src/components/InlineDatePicker.tsx new file mode 100644 index 000000000..eaf0f5dc8 --- /dev/null +++ b/packages/shared-business/src/components/InlineDatePicker.tsx @@ -0,0 +1,143 @@ +import { Box } from "@swan-io/lake/src/components/Box"; +import { InputError } from "@swan-io/lake/src/components/InputError"; +import { LakeLabel } from "@swan-io/lake/src/components/LakeLabel"; +import { LakeSelect } from "@swan-io/lake/src/components/LakeSelect"; +import { LakeTextInput } from "@swan-io/lake/src/components/LakeTextInput"; +import { Stack } from "@swan-io/lake/src/components/Stack"; +import { colors } from "@swan-io/lake/src/constants/design"; +import { isNotNullish } from "@swan-io/lake/src/utils/nullish"; +import { StyleSheet, View } from "react-native"; +import { ExtractedDate } from "../utils/date"; +import { t } from "../utils/i18n"; + +const months = [ + { value: "01", name: t("datePicker.month.january") }, + { value: "02", name: t("datePicker.month.february") }, + { value: "03", name: t("datePicker.month.march") }, + { value: "04", name: t("datePicker.month.april") }, + { value: "05", name: t("datePicker.month.may") }, + { value: "06", name: t("datePicker.month.june") }, + { value: "07", name: t("datePicker.month.july") }, + { value: "08", name: t("datePicker.month.august") }, + { value: "09", name: t("datePicker.month.september") }, + { value: "10", name: t("datePicker.month.october") }, + { value: "11", name: t("datePicker.month.november") }, + { value: "12", name: t("datePicker.month.december") }, +]; + +const styles = StyleSheet.create({ + day: { + maxWidth: 90, + flexGrow: 0, + }, + year: { + maxWidth: 120, + flexGrow: 0, + }, + error: { + borderColor: colors.negative[400], + }, +}); + +export type InlineDatePickerProps = { + label: string; + value: ExtractedDate | undefined; + onValueChange: (value: ExtractedDate) => void; + error?: string; + onBlur?: () => void; + order: "day-month-year" | "month-day-year"; +}; + +export const InlineDatePicker = ({ + value = { day: "", month: "", year: "" }, + label, + onValueChange, + error, + onBlur, + order, +}: InlineDatePickerProps) => { + return ( + { + const day = ( + + { + onValueChange({ + day, + month: value.month, + year: value.year, + }); + }} + pattern="[0-9]" + maxLength={2} + autoComplete="bday-day" + /> + + ); + + const month = ( + { + onValueChange({ + day: value.day, + month, + year: value.year, + }); + }} + /> + ); + + const year = ( + + + onValueChange({ + day: value.day, + month: value.month, + year, + }) + } + pattern="[0-9]" + maxLength={4} + autoComplete="bday-year" + /> + + ); + + return ( + + {order === "day-month-year" ? ( + + {day} {month} {year} + + ) : ( + + {month} {day} {year} + + )} + + + + ); + }} + /> + ); +}; diff --git a/packages/shared-business/src/locales/en.json b/packages/shared-business/src/locales/en.json index 0d875dbc2..c34921d29 100644 --- a/packages/shared-business/src/locales/en.json +++ b/packages/shared-business/src/locales/en.json @@ -40,6 +40,7 @@ "common.skipToContent": "Skip to content", "copyButton.copiedTooltip": "Copied to clipboard", "copyButton.copyTooltip": "Click to copy", + "datePicker.day": "Day", "datePicker.day.friday": "Friday", "datePicker.day.monday": "Monday", "datePicker.day.saturday": "Saturday", @@ -47,6 +48,7 @@ "datePicker.day.thursday": "Thursday", "datePicker.day.tuesday": "Tuesday", "datePicker.day.wednesday": "Wednesday", + "datePicker.month": "Month", "datePicker.month.april": "April", "datePicker.month.august": "August", "datePicker.month.december": "December", @@ -61,6 +63,7 @@ "datePicker.month.october": "October", "datePicker.month.previous": "Previous month", "datePicker.month.september": "September", + "datePicker.year": "Year", "error.generic": "An error occurred", "error.iban.invalid": "This IBAN doesn't look right. Trying entering it again.", "error.network.500": "Internal server error", @@ -293,5 +296,7 @@ "transactionStatement.title.creditor": "Creditor information", "transactionStatement.title.debtor": "Debtor information", "transactionStatement.title.document": "Transaction confirmation", - "transactionStatement.title.information": "Information" + "transactionStatement.title.information": "Information", + "validation.invalidBirthDate": "Invalid birthdate", + "validation.birthdateCannotBeFuture": "Birthdate cannot be in the future" } diff --git a/packages/shared-business/src/utils/date.ts b/packages/shared-business/src/utils/date.ts index 937d60cc8..b53dad761 100644 --- a/packages/shared-business/src/utils/date.ts +++ b/packages/shared-business/src/utils/date.ts @@ -1,11 +1,37 @@ import dayjs from "dayjs"; export const decodeBirthDate = (value: string) => { - const date = dayjs.utc(value, "YYYY-MM-DD"); + const date = dayjs.utc(value, "YYYY-MM-DD", true); return date.isValid() ? date.format("DD/MM/YYYY") : ""; }; export const encodeBirthDate = (value: string) => { - const date = dayjs.utc(value, "DD/MM/YYYY"); + const date = dayjs.utc(value, "DD/MM/YYYY", true); return date.isValid() ? date.format("YYYY-MM-DD") : ""; }; + +export type ExtractedDate = { + day: string; + month: string; + year: string; +}; + +export const extractDate = (value: string): ExtractedDate | undefined => { + const date = dayjs.utc(value, "YYYY-MM-DD", true); + + if (date.isValid()) { + return { + day: date.format("DD"), + month: date.format("MM"), + year: date.format("YYYY"), + }; + } +}; + +export const formatExtractedDate = (date: ExtractedDate): string => { + const day = date.day.trim().padStart(2, "0"); + const month = date.month.trim().padStart(2, "0"); + const year = date.year.trim().padStart(4, "0"); + + return `${year}-${month}-${day}`; +}; diff --git a/packages/shared-business/src/utils/validation.ts b/packages/shared-business/src/utils/validation.ts index f8539077b..a25783bfa 100644 --- a/packages/shared-business/src/utils/validation.ts +++ b/packages/shared-business/src/utils/validation.ts @@ -1,7 +1,9 @@ import { noop } from "@swan-io/lake/src/utils/function"; import { Validator } from "@swan-io/use-form"; +import dayjs from "dayjs"; import { isValid as isValidIban } from "iban"; import { match } from "ts-pattern"; +import { ExtractedDate, formatExtractedDate } from "./date"; import { t } from "./i18n"; import { AccountCountry } from "./templateTranslations"; @@ -122,3 +124,18 @@ export const validateIban = (iban: string) => { return t("error.iban.invalid"); } }; + +export const validateBirthdate = (value: ExtractedDate) => { + const date = dayjs.utc(formatExtractedDate(value), "YYYY-MM-DD", true); + + const isBirthdateOver150years = date.isBefore(dayjs.utc().subtract(150, "years")); + const isBirthdateWithin4years = date.isAfter(dayjs.utc().subtract(4, "years")); + + if (!date.isValid() || isBirthdateOver150years || isBirthdateWithin4years) { + return t("validation.invalidBirthDate"); + } + + if (date.isAfter(dayjs.utc().add(1, "day"))) { + return t("validation.birthdateCannotBeFuture"); + } +};