From dfff8fe0f7be97fb630f500f052d17e879ca817f Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 20 May 2025 11:43:54 +0300 Subject: [PATCH 01/27] feat(account-select): init commit --- packages/account-select/package.json | 26 ++++++ .../src/Component.responsive.tsx | 34 +++++++ .../src/desktop/Component.desktop.tsx | 9 ++ packages/account-select/src/desktop/index.ts | 1 + .../account-select/src/desktop/package.json | 3 + .../src/docs/Component.stories.tsx | 89 +++++++++++++++++++ packages/account-select/src/index.ts | 3 + .../src/mobile/Component.mobile.tsx | 9 ++ packages/account-select/src/mobile/index.ts | 1 + .../account-select/src/mobile/package.json | 3 + packages/account-select/src/types.ts | 5 ++ packages/account-select/tsconfig.json | 14 +++ 12 files changed, 197 insertions(+) create mode 100644 packages/account-select/package.json create mode 100644 packages/account-select/src/Component.responsive.tsx create mode 100644 packages/account-select/src/desktop/Component.desktop.tsx create mode 100644 packages/account-select/src/desktop/index.ts create mode 100644 packages/account-select/src/desktop/package.json create mode 100644 packages/account-select/src/docs/Component.stories.tsx create mode 100644 packages/account-select/src/index.ts create mode 100644 packages/account-select/src/mobile/Component.mobile.tsx create mode 100644 packages/account-select/src/mobile/index.ts create mode 100644 packages/account-select/src/mobile/package.json create mode 100644 packages/account-select/src/types.ts create mode 100644 packages/account-select/tsconfig.json diff --git a/packages/account-select/package.json b/packages/account-select/package.json new file mode 100644 index 0000000000..53423bebc7 --- /dev/null +++ b/packages/account-select/package.json @@ -0,0 +1,26 @@ +{ + "name": "@alfalab/core-components-account-select", + "version": "1.0.0", + "description": "AccountSelect component", + "keywords": [], + "license": "MIT", + "main": "index.js", + "module": "./esm/index.js", + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "sideEffects": false, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.1 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.1 || ^18.0.0" + }, + "dependencies": { + "@alfalab/core-components-select": "^17.20.13", + "@alfalab/core-components-shared": "^0.16.0", + "@alfalab/core-components-mq": "^4.4.1", + "@alfalab/hooks": "^1.13.1", + "classnames": "^2.5.1", + "tslib": "^2.4.0" + } +} diff --git a/packages/account-select/src/Component.responsive.tsx b/packages/account-select/src/Component.responsive.tsx new file mode 100644 index 0000000000..e5a45e043b --- /dev/null +++ b/packages/account-select/src/Component.responsive.tsx @@ -0,0 +1,34 @@ +import React, { forwardRef } from 'react'; + +import { useIsDesktop } from '@alfalab/core-components-mq'; + +import { AccountSelectDesktop } from './desktop'; +import { AccountSelectMobile } from './mobile'; +import type { AccountSelectProps } from './types'; + +export const AccountSelectResponsive = forwardRef( + ( + { + breakpoint, + client, + defaultMatchMediaValue = client === undefined ? undefined : client === 'desktop', + ...restProps + }, + ref, + ) => { + const isDesktop = useIsDesktop(breakpoint, defaultMatchMediaValue); + + if (isDesktop) { + return ; + } + + const mobileProps = { + ...restProps, + ...restProps.originalProps, + }; + + return ; + }, +); + +AccountSelectResponsive.displayName = 'AccountSelectResponsive'; diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx new file mode 100644 index 0000000000..ef50c1431c --- /dev/null +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { SelectDesktop, SelectDesktopProps } from '@alfalab/core-components-select/desktop'; +import { Arrow } from '@alfalab/core-components-select/shared'; + + +export const AccountSelectDesktop: React.FC = (props) => { + return
test
} Arrow={Arrow} {...props}>
; +}; diff --git a/packages/account-select/src/desktop/index.ts b/packages/account-select/src/desktop/index.ts new file mode 100644 index 0000000000..a715771d36 --- /dev/null +++ b/packages/account-select/src/desktop/index.ts @@ -0,0 +1 @@ +export { AccountSelectDesktop } from './Component.desktop'; diff --git a/packages/account-select/src/desktop/package.json b/packages/account-select/src/desktop/package.json new file mode 100644 index 0000000000..31798d54a5 --- /dev/null +++ b/packages/account-select/src/desktop/package.json @@ -0,0 +1,3 @@ +{ + "module": "../esm/desktop/index.js" +} diff --git a/packages/account-select/src/docs/Component.stories.tsx b/packages/account-select/src/docs/Component.stories.tsx new file mode 100644 index 0000000000..6d9a6ea60f --- /dev/null +++ b/packages/account-select/src/docs/Component.stories.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { AccountSelectDesktop } from '@alfalab/core-components-account-select/desktop'; + +const meta: Meta = { + title: 'Components/AccountSelect', + component: AccountSelectDesktop, + id: 'AccountSelect', + argTypes: { + size: { + control: 'select', + options: ['s', 'm', 'l'], + description: 'Размер компонента', + }, + block: { + control: 'boolean', + description: 'Растягивает компонент на ширину контейнера', + }, + label: { + control: 'text', + description: 'Текст подписи', + }, + error: { + control: 'text', + description: 'Текст ошибки', + }, + hint: { + control: 'text', + description: 'Текст подсказки', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const options = [ + { key: '1', content: 'Счет 1' }, + { key: '2', content: 'Счет 2' }, + { key: '3', content: 'Счет 3' }, +]; + +export const Default: Story = { + args: { + label: 'Выберите счет', + options, + }, +}; + +export const WithError: Story = { + args: { + label: 'Выберите счет', + error: 'Обязательное поле', + options, + }, +}; + +export const WithHint: Story = { + args: { + label: 'Выберите счет', + hint: 'Выберите счет для перевода', + options, + }, +}; + +export const Block: Story = { + args: { + label: 'Выберите счет', + block: true, + options, + }, +}; + +export const Small: Story = { + args: { + label: 'Выберите счет', + size: 's', + options, + }, +}; + +export const Large: Story = { + args: { + label: 'Выберите счет', + size: 'l', + options, + }, +}; diff --git a/packages/account-select/src/index.ts b/packages/account-select/src/index.ts new file mode 100644 index 0000000000..aecc172a45 --- /dev/null +++ b/packages/account-select/src/index.ts @@ -0,0 +1,3 @@ +export { AccountSelectDesktop } from './desktop'; +export { AccountSelectResponsive } from './Component.responsive'; +export type { AccountSelectProps } from './types'; diff --git a/packages/account-select/src/mobile/Component.mobile.tsx b/packages/account-select/src/mobile/Component.mobile.tsx new file mode 100644 index 0000000000..b094bc6950 --- /dev/null +++ b/packages/account-select/src/mobile/Component.mobile.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { SelectMobile, SelectMobileProps } from '@alfalab/core-components-select/mobile'; +import { Arrow } from '@alfalab/core-components-select/shared'; + + +export const AccountSelectMobile: React.FC = (props) => { + return
test
} Arrow={Arrow} {...props}>
; +}; diff --git a/packages/account-select/src/mobile/index.ts b/packages/account-select/src/mobile/index.ts new file mode 100644 index 0000000000..e15a0e53c2 --- /dev/null +++ b/packages/account-select/src/mobile/index.ts @@ -0,0 +1 @@ +export { default as AccountSelectMobile } from './Component.mobile'; diff --git a/packages/account-select/src/mobile/package.json b/packages/account-select/src/mobile/package.json new file mode 100644 index 0000000000..af39c0370b --- /dev/null +++ b/packages/account-select/src/mobile/package.json @@ -0,0 +1,3 @@ +{ + "module": "../esm/mobile/index.js" +} diff --git a/packages/account-select/src/types.ts b/packages/account-select/src/types.ts new file mode 100644 index 0000000000..ca96a50efd --- /dev/null +++ b/packages/account-select/src/types.ts @@ -0,0 +1,5 @@ +import { FormControlProps } from '@alfalab/core-components-form-control'; +import { SelectProps } from '@alfalab/core-components-select'; + +export type AccountSelectProps = Omit & + Pick; diff --git a/packages/account-select/tsconfig.json b/packages/account-select/tsconfig.json new file mode 100644 index 0000000000..b527eabe6d --- /dev/null +++ b/packages/account-select/tsconfig.json @@ -0,0 +1,14 @@ +{ + "include": ["src", "../../typings"], + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDirs": ["src"], + "baseUrl": ".", + "paths": { + "@alfalab/core-components-*": ["../*/src"], + "@alfalab/core-components-select/*": ["../select/src/*"] + } + }, + "references": [{ "path": "../select" }, { "path": "../shared" }, { "path": "../mq" }] +} From 0b3020ffffd846cbf4bc2fc81ff0c82dfdf9e16f Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 26 May 2025 11:38:22 +0300 Subject: [PATCH 02/27] feat(account-select): desktop version --- packages/account-select/package.json | 3 +- .../multi-step-card-input/index.module.css | 8 + .../multi-step-card-input/index.tsx | 210 ++++++++++++++++++ packages/account-select/src/constants.ts | 1 + .../src/desktop/Component.desktop.tsx | 70 +++++- .../src/docs/Component.stories.tsx | 95 ++------ .../src/mobile/Component.mobile.tsx | 24 +- packages/account-select/src/types.ts | 37 ++- packages/account-select/tsconfig.json | 16 +- yarn.lock | 9 + 10 files changed, 382 insertions(+), 91 deletions(-) create mode 100644 packages/account-select/src/components/multi-step-card-input/index.module.css create mode 100644 packages/account-select/src/components/multi-step-card-input/index.tsx create mode 100644 packages/account-select/src/constants.ts diff --git a/packages/account-select/package.json b/packages/account-select/package.json index 53423bebc7..33141ff5ef 100644 --- a/packages/account-select/package.json +++ b/packages/account-select/package.json @@ -1,6 +1,6 @@ { "name": "@alfalab/core-components-account-select", - "version": "1.0.0", + "version": "0.0.0", "description": "AccountSelect component", "keywords": [], "license": "MIT", @@ -17,6 +17,7 @@ }, "dependencies": { "@alfalab/core-components-select": "^17.20.13", + "@alfalab/core-components-popover": "^6.3.9", "@alfalab/core-components-shared": "^0.16.0", "@alfalab/core-components-mq": "^4.4.1", "@alfalab/hooks": "^1.13.1", diff --git a/packages/account-select/src/components/multi-step-card-input/index.module.css b/packages/account-select/src/components/multi-step-card-input/index.module.css new file mode 100644 index 0000000000..88f28cc7bf --- /dev/null +++ b/packages/account-select/src/components/multi-step-card-input/index.module.css @@ -0,0 +1,8 @@ +.multistepInput { + display: flex; + gap: 24px; +} + +.input { + all: unset; +} diff --git a/packages/account-select/src/components/multi-step-card-input/index.tsx b/packages/account-select/src/components/multi-step-card-input/index.tsx new file mode 100644 index 0000000000..5421c9f229 --- /dev/null +++ b/packages/account-select/src/components/multi-step-card-input/index.tsx @@ -0,0 +1,210 @@ +import React, { useEffect, useState } from 'react'; + +import { FormControlDesktop } from '@alfalab/core-components-form-control/desktop'; +import { MaskedInput } from '@alfalab/core-components-masked-input'; +import { Arrow, Field, FieldProps } from '@alfalab/core-components-select/shared'; +import { Typography } from '@alfalab/core-components-typography'; + +import { ADD_CARD_KEY } from '../../constants'; +import { AccountSelectProps } from '../../types'; + +import styles from './index.module.css'; + +export type MultiStepCardInputProps = FieldProps & Pick; + +type TField = 'card' | 'expiry' | 'cvv'; + +const fieldToStep = (field: TField) => ({ card: 0, expiry: 1, cvv: 2 }[field]); + +const nextField = (field: TField): TField | null => + ({ card: 'expiry', expiry: 'cvv', cvv: null }[field] as TField | null); + +// Маски для каждого поля +const cardMask = [ + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, +]; +const expiryMask = [/\d/, /\d/, '/', /\d/, /\d/]; +const cvvMask = [/\d/, /\d/, /\d/]; + +const getMask = (field: TField) => { + switch (field) { + case 'card': + return cardMask; + case 'expiry': + return expiryMask; + case 'cvv': + return cvvMask; + default: + return cardMask; + } +}; + +const parseExpiryDate = (value: string): Date | null => { + const [month, year] = value.split('/'); + + return new Date(2000 + parseInt(year, 10), parseInt(month, 10) - 1); +}; + +const validate = (field: TField, value: string) => { + switch (field) { + case 'card': + return /^\d{16}$/.test(value.replace(/\s/g, '')); + case 'expiry': + // Проверяем только что введены 2 цифры месяца (01-12) и 2 цифры года + return /^(0[1-9]|1[0-2])\/\d{2}$/.test(value); + case 'cvv': + return /^\d{3}$/.test(value); + default: + return false; + } +}; + +export const MultiStepCardInput: React.FC = ({ + onSubmit, + selected, + label, + innerProps, + onInput, + ...restProps +}) => { + const [step, setStep] = useState(0); + const [values, setValues] = useState<{ card: string; expiry: Date | null; cvv: string }>({ + card: '', + expiry: null, + cvv: '', + }); + const [isValid, setIsValid] = useState({ card: false, expiry: false, cvv: false }); + const [focusedField, setFocusedField] = useState<'card' | 'expiry' | 'cvv' | null>('card'); + + useEffect(() => { + if (Object.values(isValid).every(Boolean) && values.expiry !== null) { + onSubmit?.({ + number: values.card, + expiryDate: values.expiry, + cvv: values.cvv, + }); + setFocusedField(null); + } + }, [isValid, onSubmit, values]); + + const handleChange = (field: TField, value: string) => { + const valid = validate(field, value); + + if (field === 'expiry' && valid) { + const expiryDate = parseExpiryDate(value); + + setValues((prev) => ({ ...prev, [field]: expiryDate })); + } else { + setValues((prev) => ({ ...prev, [field]: value })); + } + + setIsValid((prev) => ({ ...prev, [field]: valid })); + + if (valid && step === fieldToStep(field)) { + setStep(step + 1); + setFocusedField(nextField(field)); + } + }; + + const getDisplayValue = (field: TField, value: string | Date | null) => { + switch (field) { + case 'card': + return value ? `··${(value as string).slice(-4)}` : '····'; + case 'expiry': { + if (!value) return 'MM/YY'; + const date = value as Date; + + return `${String(date.getMonth() + 1).padStart(2, '0')}/${String( + date.getFullYear(), + ).slice(-2)}`; + } + case 'cvv': + return '···'; + default: + return ''; + } + }; + + const getInputValue = (field: TField, value: string | Date | null): string => { + if (field === 'expiry' && value instanceof Date) { + return `${String(value.getMonth() + 1).padStart(2, '0')}/${String( + value.getFullYear(), + ).slice(-2)}`; + } + + return (value as string) || ''; + }; + + const renderInput = (field: TField) => { + const isVisible = fieldToStep(field) <= step; + const isEditing = focusedField === field; + + if (!isVisible) return null; + + return isEditing ? ( + { + // Не блокируем всплытие события + setFocusedField(field); + }} + onChange={(e) => handleChange(field, e.target.value)} + onBlur={() => { + if (isValid[field]) setFocusedField(null); + }} + type={field === 'cvv' ? 'password' : 'text'} + /> + ) : ( + { + // Не блокируем всплытие события + setFocusedField(field); + }} + > + {getDisplayValue(field, values[field])} + + ); + }; + + if (selected?.key === ADD_CARD_KEY) { + return ( +
+ {renderInput('card')} + {renderInput('expiry')} + {renderInput('cvv')} + +
+ ); + } + + return ( + + ); +}; diff --git a/packages/account-select/src/constants.ts b/packages/account-select/src/constants.ts new file mode 100644 index 0000000000..5dac12d4b5 --- /dev/null +++ b/packages/account-select/src/constants.ts @@ -0,0 +1 @@ +export const ADD_CARD_KEY = 'add-card'; diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index ef50c1431c..5e78c49e47 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -1,9 +1,67 @@ -import React from 'react'; +import React, { forwardRef, useMemo } from 'react'; -import { SelectDesktop, SelectDesktopProps } from '@alfalab/core-components-select/desktop'; -import { Arrow } from '@alfalab/core-components-select/shared'; +import { Popover } from '@alfalab/core-components-popover'; +import { + AnyObject, + Arrow, + BaseSelect, + Optgroup as DefaultOptgroup, + Option as DefaultOption, + OptionsList as DefaultOptionsList, +} from '@alfalab/core-components-select/shared'; +import { MultiStepCardInput } from '../components/multi-step-card-input'; +import { ADD_CARD_KEY } from '../constants'; +import { AccountSelectProps } from '../types'; -export const AccountSelectDesktop: React.FC = (props) => { - return
test
} Arrow={Arrow} {...props}>
; -}; +export const AccountSelectDesktop = forwardRef( + ( + { + OptionsList = DefaultOptionsList, + Optgroup = DefaultOptgroup, + Option = DefaultOption, + onInput, + hasNewCardAdding, + onSubmit, + closeOnSelect = true, + options, + ...restProps + }, + ref, + ) => { + + const enhancedOptions = useMemo(() => { + if (!hasNewCardAdding) return options; + + return [ + { + key: ADD_CARD_KEY, + content: 'Новая карта', + value: ADD_CARD_KEY, + }, + ...options, + ]; + }, [hasNewCardAdding, options]); + + return ( + + ); + }, +); diff --git a/packages/account-select/src/docs/Component.stories.tsx b/packages/account-select/src/docs/Component.stories.tsx index 6d9a6ea60f..8eba89cdc0 100644 --- a/packages/account-select/src/docs/Component.stories.tsx +++ b/packages/account-select/src/docs/Component.stories.tsx @@ -1,89 +1,36 @@ import React from 'react'; -import type { Meta, StoryObj } from '@storybook/react'; -import { AccountSelectDesktop } from '@alfalab/core-components-account-select/desktop'; +import { Meta, StoryObj } from '@storybook/react'; +import { AccountSelectDesktop } from '../desktop'; const meta: Meta = { title: 'Components/AccountSelect', component: AccountSelectDesktop, - id: 'AccountSelect', + tags: ['autodocs'], argTypes: { - size: { - control: 'select', - options: ['s', 'm', 'l'], - description: 'Размер компонента', - }, - block: { - control: 'boolean', - description: 'Растягивает компонент на ширину контейнера', - }, - label: { - control: 'text', - description: 'Текст подписи', - }, - error: { - control: 'text', - description: 'Текст ошибки', - }, - hint: { - control: 'text', - description: 'Текст подсказки', - }, + hasNewCardAdding: { + control: 'boolean', + description: 'Включить возможность добавления новой карты', + } }, }; export default meta; - -type Story = StoryObj; +type Story = StoryObj; const options = [ - { key: '1', content: 'Счет 1' }, - { key: '2', content: 'Счет 2' }, - { key: '3', content: 'Счет 3' }, -]; - -export const Default: Story = { - args: { - label: 'Выберите счет', - options, - }, -}; - -export const WithError: Story = { - args: { - label: 'Выберите счет', - error: 'Обязательное поле', - options, - }, -}; - -export const WithHint: Story = { - args: { - label: 'Выберите счет', - hint: 'Выберите счет для перевода', - options, + { + key: '1', + content: 'Карта *1234', + value: '1' }, -}; - -export const Block: Story = { - args: { - label: 'Выберите счет', - block: true, - options, - }, -}; - -export const Small: Story = { - args: { - label: 'Выберите счет', - size: 's', - options, - }, -}; + { + key: '2', + content: 'Карта *5678', + value: '2' + } +]; -export const Large: Story = { - args: { - label: 'Выберите счет', - size: 'l', - options, - }, +export const account_select_desktop: Story = { + name: 'AccountSelectDesktop', + render: () => , }; diff --git a/packages/account-select/src/mobile/Component.mobile.tsx b/packages/account-select/src/mobile/Component.mobile.tsx index b094bc6950..3ba5487dab 100644 --- a/packages/account-select/src/mobile/Component.mobile.tsx +++ b/packages/account-select/src/mobile/Component.mobile.tsx @@ -1,9 +1,25 @@ import React from 'react'; -import { SelectMobile, SelectMobileProps } from '@alfalab/core-components-select/mobile'; -import { Arrow } from '@alfalab/core-components-select/shared'; +import { Select } from '@alfalab/core-components-select'; +import { AccountSelectProps } from '../types'; -export const AccountSelectMobile: React.FC = (props) => { - return
test
} Arrow={Arrow} {...props}>
; +export const AccountSelectMobile: React.FC = ({ + isAddingNewCard, + options, + onChange, + ...restProps +}) => { + + + return ( + + {step >= 2 && ( + + )} + {step >= 3 && ( + + )} + ); }; diff --git a/packages/account-select/src/constants.ts b/packages/account-select/src/constants.ts index 5dac12d4b5..adba99906a 100644 --- a/packages/account-select/src/constants.ts +++ b/packages/account-select/src/constants.ts @@ -1 +1,26 @@ -export const ADD_CARD_KEY = 'add-card'; +export const ADD_CARD_KEY = '#ADD_NEW_CARD'; + +// Маски для каждого поля +export const CARD_MASK = [ + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, +]; +export const EXPIRY_MASK = [/\d/, /\d/, '/', /\d/, /\d/]; +export const CVV_MASK = [/\d/, /\d/, /\d/]; diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index 5e78c49e47..357c347911 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -10,7 +10,7 @@ import { OptionsList as DefaultOptionsList, } from '@alfalab/core-components-select/shared'; -import { MultiStepCardInput } from '../components/multi-step-card-input'; +import { CustomField } from '../components/custom-field'; import { ADD_CARD_KEY } from '../constants'; import { AccountSelectProps } from '../types'; @@ -29,7 +29,6 @@ export const AccountSelectDesktop = forwardRef { - const enhancedOptions = useMemo(() => { if (!hasNewCardAdding) return options; @@ -51,7 +50,7 @@ export const AccountSelectDesktop = forwardRef = { title: 'Components/AccountSelect', component: AccountSelectDesktop, tags: ['autodocs'], argTypes: { - hasNewCardAdding: { - control: 'boolean', - description: 'Включить возможность добавления новой карты', - } + hasNewCardAdding: { + control: 'boolean', + description: 'Включить возможность добавления новой карты', + }, }, }; @@ -20,17 +23,66 @@ type Story = StoryObj; const options = [ { key: '1', - content: 'Карта *1234', - value: '1' + content: ( + + + + + + + + Альфа-карта с преимуществами + + + + + + ), + value: '1', }, { key: '2', - content: 'Карта *5678', - value: '2' - } + content: ( + + + + + + + + Альфа-карта с кредитным лимитом + + + + + + ), + value: '2', + }, ]; export const account_select_desktop: Story = { name: 'AccountSelectDesktop', - render: () => , + render: () => ( + console.log(values)} + label='Выберите карту' + options={options} + hasNewCardAdding={true} + /> + ), }; diff --git a/packages/account-select/src/types.ts b/packages/account-select/src/types.ts index 4987acdd92..f59793936a 100644 --- a/packages/account-select/src/types.ts +++ b/packages/account-select/src/types.ts @@ -2,7 +2,7 @@ import { BaseSelectProps } from '@alfalab/core-components-select/shared'; export interface CardData { number: string; - expiryDate: Date; + expiryDate: string; cvv: string; } @@ -31,4 +31,4 @@ export interface AccountSelectProps * Обработчик отправки новой карты */ onSubmit?: (value: CardData) => void; -} \ No newline at end of file +} diff --git a/packages/account-select/src/utils/validate/index.ts b/packages/account-select/src/utils/validate/index.ts new file mode 100644 index 0000000000..bb5eab631a --- /dev/null +++ b/packages/account-select/src/utils/validate/index.ts @@ -0,0 +1,20 @@ +export const validateCardNumber = (value: string) => value.replace(/\s/g, '').length === 16; + +export const validateExpiry = (value: string) => { + if (value.length !== 5) return false; + const [month, year] = value.split('/'); + const currentYear = new Date().getFullYear() % 100; + const currentMonth = new Date().getMonth() + 1; + + const monthNum = parseInt(month, 10); + const yearNum = parseInt(year, 10); + + return ( + monthNum >= 1 && + monthNum <= 12 && + yearNum >= currentYear && + (yearNum > currentYear || monthNum >= currentMonth) + ); +}; + +export const validateCvv = (value: string) => value.length === 3; diff --git a/packages/account-select/tsconfig.json b/packages/account-select/tsconfig.json index 08d9b575df..95d1b39461 100644 --- a/packages/account-select/tsconfig.json +++ b/packages/account-select/tsconfig.json @@ -9,18 +9,18 @@ "@alfalab/core-components-*": ["../*/src"], "@alfalab/core-components-select/*": ["../select/src/*"], "@alfalab/core-components-popover/*": ["../popover/src/*"], - "@alfalab/core-components-masked-input/*": ["../masked-input/src/*"], - "@alfalab/core-components-typography/*": ["../typography/src/*"], - "@alfalab/core-components-form-control/*": ["../form-control/src/*"] + "@alfalab/core-components-form-control/*": ["../form-control/src/*"], + "@alfalab/core-components-product-cover/*": ["../product-cover/src/*"], + "@alfalab/core-components-button/*": ["../button/src/*"] } }, "references": [ { "path": "../shared" }, { "path": "../mq" }, { "path": "../select" }, - { "path": "../masked-input" }, { "path": "../popover" }, - { "path": "../typography" }, - { "path": "../form-control" } + { "path": "../form-control" }, + { "path": "../product-cover" }, + { "path": "../button" } ] } From 3328d44138b230c83ca32c069f2a6fff8004dd2c Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 25 Jun 2025 18:35:39 +0300 Subject: [PATCH 04/27] =?UTF-8?q?feat(account-select):=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=20AccountSelec?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/gold-panthers-swim.md | 5 + packages/account-select/package.json | 3 +- .../src/Component.responsive.tsx | 27 +- .../src/components/custom-field/index.tsx | 29 +- .../multi-step-card-input/index.module.css | 4 +- .../multi-step-card-input/index.tsx | 368 +++++++++-------- packages/account-select/src/constants.ts | 57 +-- packages/account-select/src/context.ts | 9 + .../src/desktop/Component.desktop.tsx | 70 ++-- .../src/desktop/index.module.css | 9 + .../src/docs/Component.docs.mdx | 19 + .../src/docs/Component.stories.tsx | 111 ++++- .../account-select/src/docs/description.mdx | 389 ++++++++++++++++++ .../account-select/src/docs/development.mdx | 20 + packages/account-select/src/index.ts | 1 - .../src/mobile/Component.mobile.tsx | 68 ++- packages/account-select/src/mobile/index.ts | 2 +- packages/account-select/src/types.ts | 65 ++- .../src/utils/formaters/index.test.ts | 69 ++++ .../src/utils/formaters/index.ts | 13 + .../src/utils/parse-date/index.test.ts | 86 ++++ .../src/utils/parse-date/index.ts | 26 ++ .../src/utils/validate/index.test.ts | 129 ++++++ .../src/utils/validate/index.ts | 10 +- packages/account-select/tsconfig.json | 2 - 25 files changed, 1291 insertions(+), 300 deletions(-) create mode 100644 .changeset/gold-panthers-swim.md create mode 100644 packages/account-select/src/context.ts create mode 100644 packages/account-select/src/desktop/index.module.css create mode 100644 packages/account-select/src/docs/Component.docs.mdx create mode 100644 packages/account-select/src/docs/description.mdx create mode 100644 packages/account-select/src/docs/development.mdx create mode 100644 packages/account-select/src/utils/formaters/index.test.ts create mode 100644 packages/account-select/src/utils/formaters/index.ts create mode 100644 packages/account-select/src/utils/parse-date/index.test.ts create mode 100644 packages/account-select/src/utils/parse-date/index.ts create mode 100644 packages/account-select/src/utils/validate/index.test.ts diff --git a/.changeset/gold-panthers-swim.md b/.changeset/gold-panthers-swim.md new file mode 100644 index 0000000000..aa2fc46e72 --- /dev/null +++ b/.changeset/gold-panthers-swim.md @@ -0,0 +1,5 @@ +--- +'@alfalab/core-components-account-select': major +--- + +Добавлен новый компонент AccountSelect diff --git a/packages/account-select/package.json b/packages/account-select/package.json index 2fe40196a3..395face1ad 100644 --- a/packages/account-select/package.json +++ b/packages/account-select/package.json @@ -22,8 +22,9 @@ "@alfalab/core-components-mq": "^4.4.1", "@alfalab/core-components-masked-input": "^6.3.21", "@alfalab/hooks": "^1.13.1", + "@maskito/core": "^1.7.0", + "@maskito/react": "^1.7.0", "classnames": "^2.5.1", - "text-mask-core": "^5.1.2", "tslib": "^2.4.0" } } diff --git a/packages/account-select/src/Component.responsive.tsx b/packages/account-select/src/Component.responsive.tsx index e5a45e043b..e82030ea5b 100644 --- a/packages/account-select/src/Component.responsive.tsx +++ b/packages/account-select/src/Component.responsive.tsx @@ -4,30 +4,17 @@ import { useIsDesktop } from '@alfalab/core-components-mq'; import { AccountSelectDesktop } from './desktop'; import { AccountSelectMobile } from './mobile'; -import type { AccountSelectProps } from './types'; +import type { AccountSelectResonsiveProps } from './types'; -export const AccountSelectResponsive = forwardRef( - ( - { - breakpoint, - client, - defaultMatchMediaValue = client === undefined ? undefined : client === 'desktop', - ...restProps - }, - ref, - ) => { - const isDesktop = useIsDesktop(breakpoint, defaultMatchMediaValue); +export const AccountSelectResponsive = forwardRef( + ({ breakpoint, client, ...restProps }, ref) => { + const isDesktop = useIsDesktop(breakpoint); - if (isDesktop) { - return ; + if (isDesktop || client === 'desktop') { + return ; } - const mobileProps = { - ...restProps, - ...restProps.originalProps, - }; - - return ; + return ; }, ); diff --git a/packages/account-select/src/components/custom-field/index.tsx b/packages/account-select/src/components/custom-field/index.tsx index 4f0350b474..b485b9db57 100644 --- a/packages/account-select/src/components/custom-field/index.tsx +++ b/packages/account-select/src/components/custom-field/index.tsx @@ -1,33 +1,54 @@ import React, { useCallback } from 'react'; import { FormControlDesktop } from '@alfalab/core-components-form-control/desktop'; +import { FormControlMobile } from '@alfalab/core-components-form-control/mobile'; import { Field, FieldProps, OptionShape } from '@alfalab/core-components-select/shared'; import { ADD_CARD_KEY } from '../../constants'; import { MultiStepCardInput } from '../multi-step-card-input'; +export interface CustomFieldProps extends FieldProps { + view?: 'desktop' | 'mobile'; +} + export const CustomField = ({ innerProps, selected, label, onSubmit, onInput, + needCvv, + needExpiryDate, + expiryAsDate, + view = 'desktop', + cardImage, ...restProps -}: FieldProps) => { +}: CustomFieldProps) => { const valueRenderer = useCallback( ({ selected: selectedOption }: { selected?: OptionShape }) => { if (selectedOption?.key === ADD_CARD_KEY) { - return ; + return ( + + ); } return selectedOption?.content; }, - [onSubmit, onInput], + [onSubmit, onInput, cardImage, needCvv, needExpiryDate, expiryAsDate], ); + const FormControlComponent = view === 'mobile' ? FormControlMobile : FormControlDesktop; + return ( ; - -const createMaskConfig = (mask: TextMaskConfig['mask']): TextMaskConfig => ({ - mask, - guide: false, - keepCharPositions: false, - showMask: false, - currentCaretPosition: 0, - rawValue: '', - previousConformedValue: '', -}); - -const getMaskedCardNumber = (value: string) => { - if (!value || value.length < 16) return value; - const cleanValue = value.replace(/\s/g, ''); - const lastFour = cleanValue.slice(-4); - - return `··${lastFour}`; -}; - -export const MultiStepCardInput: React.FC = ({ onSubmit, onInput }) => { - const [step, setStep] = useState(1); - const [cardNumber, setCardNumber] = useState(''); - const [cardExpiry, setCardExpiry] = useState(''); - const [cardCvv, setCardCvv] = useState(''); - const [isCardNumberFocused, setIsCardNumberFocused] = useState(false); - - const numberRef = useRef(null); - const expiryRef = useRef(null); - const cvvRef = useRef(null); - - const numberMask = useRef(null); - const expiryMask = useRef(null); - const cvvMask = useRef(null); - - useEffect(() => { - onInput?.({ - number: cardNumber, - expiryDate: cardExpiry, - cvv: cardCvv, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cardNumber, cardExpiry, cardCvv]); - - useEffect(() => { - if (step === 1) { - numberRef.current?.focus(); - } - }, [step]); - - useEffect(() => { - if (numberRef.current) { - numberMask.current = createTextMaskInputElement({ - ...createMaskConfig(CARD_MASK), - inputElement: numberRef.current, +type MultiStepCardInputProps = Pick< + CardAddingProps, + 'onSubmit' | 'onInput' | 'cardImage' | 'needCvv' | 'needExpiryDate' | 'expiryAsDate' +>; + +export const MultiStepCardInput: React.FC = memo( + ({ + onSubmit, + onInput, + cardImage, + needCvv = true, + needExpiryDate = true, + expiryAsDate = true, + }) => { + const [step, setStep] = useState(1); + const [cardNumber, setCardNumber] = useState(''); + const [cardExpiry, setCardExpiry] = useState(''); + const [cardCvv, setCardCvv] = useState(''); + const [isCardNumberFocused, setIsCardNumberFocused] = useState(false); + + const { setError } = useAccountSelectContext(); + + const numberRef = useRef(null); + const expiryRef = useRef(null); + const cvvRef = useRef(null); + + const numberMaskRef = useMaskito({ options: CARD_MASK }); + const expiryMaskRef = useMaskito({ options: EXPIRY_MASK }); + const cvvMaskRef = useMaskito({ options: CVV_MASK }); + + const numberRefCallback = useCallback( + (element: HTMLInputElement | null) => { + (numberRef as React.MutableRefObject).current = element; + numberMaskRef(element); + }, + [numberMaskRef], + ); + + const expiryRefCallback = useCallback( + (element: HTMLInputElement | null) => { + (expiryRef as React.MutableRefObject).current = element; + expiryMaskRef(element); + }, + [expiryMaskRef], + ); + + const cvvRefCallback = useCallback( + (element: HTMLInputElement | null) => { + (cvvRef as React.MutableRefObject).current = element; + cvvMaskRef(element); + }, + [cvvMaskRef], + ); + + useEffect(() => { + onInput?.({ + number: cardNumber, + ...(needExpiryDate && { expiryDate: cardExpiry }), + ...(needCvv && cardCvv && { cvv: cardCvv }), }); - } - }, []); - - useEffect(() => { - if (expiryRef.current && step >= 2) { - expiryMask.current = createTextMaskInputElement({ - ...createMaskConfig(EXPIRY_MASK), - inputElement: expiryRef.current, - }); - } - }, [step]); - - useEffect(() => { - if (cvvRef.current && step >= 3) { - cvvMask.current = createTextMaskInputElement({ - ...createMaskConfig(CVV_MASK), - inputElement: cvvRef.current, - }); - } - }, [step]); - - const handleCardNumberFocus = () => { - setIsCardNumberFocused(true); - }; - - const handleCardNumberBlur = () => { - setIsCardNumberFocused(false); - }; - - const handleCardNumberChange = ({ target: { value } }: React.ChangeEvent) => { - if (numberMask.current) { - numberMask.current.update(value); - const maskedValue = numberRef.current?.value || ''; - setCardNumber(maskedValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cardNumber, cardExpiry, cardCvv, needExpiryDate, needCvv]); - if (validateCardNumber(maskedValue)) { - setStep(2); - setTimeout(() => expiryRef.current?.focus(), 100); + useEffect(() => { + if (step === 1) { + numberRef.current?.focus(); } - } - }; - - const handleExpiryChange = ({ target: { value } }: React.ChangeEvent) => { - if (expiryMask.current) { - expiryMask.current.update(value); - const maskedValue = expiryRef.current?.value || ''; - - setCardExpiry(maskedValue); - - if (validateExpiry(maskedValue)) { - setStep(3); - setTimeout(() => cvvRef.current?.focus(), 100); + }, [step]); + + const handleCardNumberFocus = () => { + setIsCardNumberFocused(true); + setTimeout(() => { + const { current } = numberRef; + + if (current) { + const { length } = current.value; + + current.setSelectionRange(length, length); + } + }, 0); + }; + + const handleCardNumberBlur = (e: React.FocusEvent) => { + setIsCardNumberFocused(false); + setError(validateCardNumber(e.target.value) ? null : 'Номер карты введён неверно'); + }; + + const handleExpiryBlur = (e: React.FocusEvent) => { + setError(validateExpiry(e.target.value) ? null : 'Срок действия карты введён неверно'); + }; + + const handleCvvBlur = (e: React.FocusEvent) => { + setError(validateCvv(e.target.value) ? null : 'Нужно заполнить CVV'); + }; + + const handleCardNumberChange = ({ + target: { value }, + }: React.ChangeEvent) => { + const cleanValue = value.replace(/\s/g, ''); + + setCardNumber(cleanValue); + + if (validateCardNumber(cleanValue)) { + if (needExpiryDate) { + setStep(2); + setTimeout(() => expiryRef.current?.focus(), 100); + } else { + numberRef.current?.blur(); + onSubmit?.({ + number: cleanValue, + }); + } } - } - }; - - const handleCvvChange = ({ target: { value } }: React.ChangeEvent) => { - if (cvvMask.current) { - cvvMask.current.update(value); - const maskedValue = cvvRef.current?.value || ''; + }; + + const handleExpiryChange = ({ target: { value } }: React.ChangeEvent) => { + setCardExpiry(value); + + if (validateExpiry(value)) { + if (needCvv) { + setStep(3); + setTimeout(() => cvvRef.current?.focus(), 100); + } else { + expiryRef.current?.blur(); + onSubmit?.({ + number: cardNumber, + expiryDate: expiryAsDate ? parseDate(value as string) : value, + }); + } + } + }; - setCardCvv(maskedValue); + const handleCvvChange = ({ target: { value } }: React.ChangeEvent) => { + setCardCvv(value); - if (validateCvv(maskedValue)) { + if (validateCvv(value)) { + cvvRef.current?.blur(); onSubmit?.({ number: cardNumber, - expiryDate: cardExpiry, - cvv: maskedValue, + expiryDate: expiryAsDate ? parseDate(cardExpiry as string) : cardExpiry, + cvv: value, }); } - } - }; - - const getDisplayCardNumber = () => { - if (isCardNumberFocused || step === 1 || !validateCardNumber(cardNumber)) { - return cardNumber; - } - - return getMaskedCardNumber(cardNumber); - }; - - return ( -
e.stopPropagation()} - aria-hidden='true' - > - - - {step >= 2 && ( - { + if (isCardNumberFocused || !validateCardNumber(cardNumber)) { + return formatCardNumber(cardNumber); + } + + return getMaskedCardNumber(cardNumber); + }; + + return ( +
e.stopPropagation()} + aria-hidden='true' + > + 5 ? Number(cardNumber) : undefined} + size={32} + {...cardImage} /> - )} - {step >= 3 && ( - )} -
- ); -}; + {needExpiryDate && step >= 2 && ( + + )} + {needCvv && step >= 3 && ( + + )} +
+ ); + }, +); diff --git a/packages/account-select/src/constants.ts b/packages/account-select/src/constants.ts index adba99906a..cc98629d4f 100644 --- a/packages/account-select/src/constants.ts +++ b/packages/account-select/src/constants.ts @@ -1,26 +1,35 @@ +import { MaskitoOptions } from '@maskito/core'; + export const ADD_CARD_KEY = '#ADD_NEW_CARD'; -// Маски для каждого поля -export const CARD_MASK = [ - /\d/, - /\d/, - /\d/, - /\d/, - ' ', - /\d/, - /\d/, - /\d/, - /\d/, - ' ', - /\d/, - /\d/, - /\d/, - /\d/, - ' ', - /\d/, - /\d/, - /\d/, - /\d/, -]; -export const EXPIRY_MASK = [/\d/, /\d/, '/', /\d/, /\d/]; -export const CVV_MASK = [/\d/, /\d/, /\d/]; +export const CARD_MASK: MaskitoOptions = { + mask: [ + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ' ', + /\d/, + /\d/, + /\d/, + /\d/, + ], +}; + +export const EXPIRY_MASK: MaskitoOptions = { + mask: [/\d/, /\d/, '/', /\d/, /\d/], +}; + +export const CVV_MASK: MaskitoOptions = { + mask: [/\d/, /\d/, /\d/], +}; diff --git a/packages/account-select/src/context.ts b/packages/account-select/src/context.ts new file mode 100644 index 0000000000..076ab71274 --- /dev/null +++ b/packages/account-select/src/context.ts @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; + +export const AccountSelectContext = createContext<{ + setError: (error: string | null) => void; +}>({ + setError: () => {}, +}); + +export const useAccountSelectContext = () => useContext(AccountSelectContext); diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index 357c347911..234038cd30 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -1,66 +1,84 @@ -import React, { forwardRef, useMemo } from 'react'; +import React, { forwardRef, useMemo, useState } from 'react'; import { Popover } from '@alfalab/core-components-popover'; import { AnyObject, Arrow, + BaseOption, BaseSelect, Optgroup as DefaultOptgroup, - Option as DefaultOption, + OptionProps, OptionsList as DefaultOptionsList, } from '@alfalab/core-components-select/shared'; import { CustomField } from '../components/custom-field'; import { ADD_CARD_KEY } from '../constants'; +import { AccountSelectContext } from '../context'; import { AccountSelectProps } from '../types'; +import styles from './index.module.css'; + +const DefaultOption = (props: OptionProps) => ( + +
{props.option.content}
+
+); + export const AccountSelectDesktop = forwardRef( ( { OptionsList = DefaultOptionsList, Optgroup = DefaultOptgroup, Option = DefaultOption, - onInput, - hasNewCardAdding, - onSubmit, closeOnSelect = true, options, + cardAddingProps, + dataTestId, ...restProps }, ref, ) => { + const [error, setError] = useState(null); + const { content, ...restCardAddingProps } = cardAddingProps || {}; const enhancedOptions = useMemo(() => { - if (!hasNewCardAdding) return options; + if (!content) return options; return [ { key: ADD_CARD_KEY, - content: 'Новая карта', + content, value: ADD_CARD_KEY, }, ...options, ]; - }, [hasNewCardAdding, options]); + }, [content, options]); + + const contextValue = useMemo(() => ({ setError }), [setError]); return ( - + + + ); }, ); diff --git a/packages/account-select/src/desktop/index.module.css b/packages/account-select/src/desktop/index.module.css new file mode 100644 index 0000000000..a1050cec13 --- /dev/null +++ b/packages/account-select/src/desktop/index.module.css @@ -0,0 +1,9 @@ +@import '../../../vars/src/index.css'; + +.accountSelect { + min-width: 400px; +} + +.optionContent { + padding-left: var(--gap-12); +} diff --git a/packages/account-select/src/docs/Component.docs.mdx b/packages/account-select/src/docs/Component.docs.mdx new file mode 100644 index 0000000000..7ed960c6f3 --- /dev/null +++ b/packages/account-select/src/docs/Component.docs.mdx @@ -0,0 +1,19 @@ +import { Meta, Markdown } from '@storybook/addon-docs'; +import { ComponentHeader, Tabs } from 'storybook/blocks'; +import * as Stories from './Component.stories'; + +import Description from './description.mdx'; +import Development from './development.mdx'; + + + + + +} + + development={} +/> \ No newline at end of file diff --git a/packages/account-select/src/docs/Component.stories.tsx b/packages/account-select/src/docs/Component.stories.tsx index cfa18b6218..be4ee3d773 100644 --- a/packages/account-select/src/docs/Component.stories.tsx +++ b/packages/account-select/src/docs/Component.stories.tsx @@ -1,23 +1,25 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { boolean, text } from '@storybook/addon-knobs'; import { AccountSelectDesktop } from '../desktop'; -import { PureCell } from '../../../pure-cell/src'; -import { ProductCover } from '../../../product-cover/src'; -import { Amount } from '../../../amount/src'; +import { PureCell } from '@alfalab/core-components-pure-cell'; +import { ProductCover } from '@alfalab/core-components-product-cover'; +import { PlusMIcon } from '@alfalab/icons-glyph/PlusMIcon'; +import { AccountSelectMobile } from '../mobile'; +import { CardData } from '../types'; + +const baseCard = { + baseUrl: 'https://online.alfabank.ru/cards-images/cards/', + layers: 'BACKGROUND,LOGO,PAYMENT_SYSTEM', + cardId: 'RM', +}; const meta: Meta = { title: 'Components/AccountSelect', component: AccountSelectDesktop, - tags: ['autodocs'], - argTypes: { - hasNewCardAdding: { - control: 'boolean', - description: 'Включить возможность добавления новой карты', - }, - }, + id: 'AccountSelect', }; -export default meta; type Story = StoryObj; const options = [ @@ -45,7 +47,6 @@ const options = [ ), - value: '1', }, { key: '2', @@ -71,18 +72,92 @@ const options = [ ), - value: '2', }, ]; export const account_select_desktop: Story = { name: 'AccountSelectDesktop', + render: () => { + const [cardImage, setCardImage] = useState(undefined); + const handleInput = (data: CardData) => { + if (data.number.startsWith('111111')) { + setCardImage(baseCard); + } + }; + const handleSubmit = (data: CardData) => { + console.log(data); + }; + + return ( + { + console.log(e); + }} + label={text('label', 'Выберите карту')} + fieldProps={{ leftAddons: }} + cardAddingProps={{ + content: ( + + + + + + + + Новая карта + + + + + ), + onInput: handleInput, + onSubmit: handleSubmit, + needCvv: boolean('needCvv', true), + needExpiryDate: boolean('needExpiryDate', true), + expiryAsDate: boolean('expiryAsDate', true), + cardImage, + }} + options={options} + /> + ); + }, +}; + +export const account_select_mobile: Story = { + name: 'AccountSelectMobile', render: () => ( - console.log(values)} - label='Выберите карту' + + + + + + + + Новая карта + + + + + ), + needCvv: boolean('needCvv', true), + needExpiryDate: boolean('needExpiryDate', true), + expiryAsDate: boolean('expiryAsDate', true), + }} /> ), }; + +export default meta; diff --git a/packages/account-select/src/docs/description.mdx b/packages/account-select/src/docs/description.mdx new file mode 100644 index 0000000000..6d9ecc0c4f --- /dev/null +++ b/packages/account-select/src/docs/description.mdx @@ -0,0 +1,389 @@ +## Выбор счёта + +```jsx live +render(() => { + const options = [ + { + key: '1', + content: ( + + + + + Экспресс-счёт ··1234 + + + + + + ), + }, + { + key: '2', + content: ( + + + + + Экспресс-счёт ··1111 + + + + + + ), + }, + { + key: '3', + content: ( + + + + + Экспресс-счёт ··4567 + + + + + + ), + }, + ]; + return ; +}); +``` + +## Выбор карты + +```jsx live +render(() => { + const options = [ + { + key: '1', + content: ( + + + + + + + + Альфа-карта с преимуществами + + + + + + ), + }, + { + key: '2', + content: ( + + + + + + + + Альфа-карта с кредитным лимитом + + + + + + ), + }, + ]; + return ( + }} + options={options} + /> + ); +}); +``` + +## Добавление новой карты отправителя + +Для карты отправителя можно использовать механику с запросом CVC или без него. +После ввода 6-го символа определяется банк и платёжная система. При необходимости, можно настроить отображение различных рубашек карт для банков РФ. В примере для карты начинающейся с номера 1234 56 будет определён банк и изменится рубашка. + +```jsx live +render(() => { + const options = [ + { + key: '1', + content: ( + + + + + + + + Альфа-карта с преимуществами + + + + + + ), + }, + { + key: '2', + content: ( + + + + + + + + Альфа-карта с кредитным лимитом + + + + + + ), + }, + ]; + + const [cvv, setCvv] = React.useState(true); + const [expiry, setExpiry] = React.useState(true); + const [cardImage, setCardImage] = React.useState(undefined); + const handleInput = (data) => { + if (data.number.startsWith('123456')) { + setCardImage({ + baseUrl: 'https://online.alfabank.ru/cards-images/cards/', + layers: 'BACKGROUND,LOGO,PAYMENT_SYSTEM', + cardId: 'RM', + }); + } + }; + + return ( +
+ }} + options={options} + cardAddingProps={{ + content: ( + + + + + + + + Новая карта + + + + + ), + needCvv: cvv, + needExpiryDate: expiry, + onInput: handleInput, + cardImage, + }} + /> + + { + setCvv((prevState) => !prevState); + }} + /> + { + setExpiry((prevState) => !prevState); + }} + /> +
+ ); +}); +``` + +## Добавление новой карты получателя + +```jsx live +render(() => { + const options = [ + { + key: '1', + content: ( + + + + + + + + Альфа-карта с преимуществами + + + + + + ), + }, + { + key: '2', + content: ( + + + + + + + + Альфа-карта с кредитным лимитом + + + + + + ), + }, + ]; + + const [cardImage, setCardImage] = React.useState(undefined); + const handleInput = (data) => { + if (data.number.startsWith('123456')) { + setCardImage({ + baseUrl: 'https://online.alfabank.ru/cards-images/cards/', + layers: 'BACKGROUND,LOGO,PAYMENT_SYSTEM', + cardId: 'RM', + }); + } + }; + + return ( +
+ }} + options={options} + cardAddingProps={{ + content: ( + + + + + + + + Новая карта + + + + + ), + needCvv: false, + needExpiryDate: false, + onInput: handleInput, + cardImage, + }} + /> +
+ ); +}); +``` diff --git a/packages/account-select/src/docs/development.mdx b/packages/account-select/src/docs/development.mdx new file mode 100644 index 0000000000..3688b1c91f --- /dev/null +++ b/packages/account-select/src/docs/development.mdx @@ -0,0 +1,20 @@ +import { ArgsTabs } from 'storybook/blocks'; +import { AccountSelectMobile } from '../mobile'; +import { AccountSelectDesktop } from '../desktop'; + +## Подключение + +```jsx +import { AccountSelectResponsive } from '@alfalab/core-components/account-select'; +import { AccountSelectDesktop } from '@alfalab/core-components/account-select/desktop'; +import { AccountSelectMobile } from '@alfalab/core-components/account-select/mobile'; +``` + +## Свойства + + diff --git a/packages/account-select/src/index.ts b/packages/account-select/src/index.ts index aecc172a45..b912938862 100644 --- a/packages/account-select/src/index.ts +++ b/packages/account-select/src/index.ts @@ -1,3 +1,2 @@ -export { AccountSelectDesktop } from './desktop'; export { AccountSelectResponsive } from './Component.responsive'; export type { AccountSelectProps } from './types'; diff --git a/packages/account-select/src/mobile/Component.mobile.tsx b/packages/account-select/src/mobile/Component.mobile.tsx index 3ba5487dab..f648a3e5b1 100644 --- a/packages/account-select/src/mobile/Component.mobile.tsx +++ b/packages/account-select/src/mobile/Component.mobile.tsx @@ -1,25 +1,51 @@ -import React from 'react'; +import React, { forwardRef, useMemo, useState } from 'react'; -import { Select } from '@alfalab/core-components-select'; +import { SelectMobile } from '@alfalab/core-components-select/mobile'; +import { CustomField, CustomFieldProps } from '../components/custom-field'; +import { ADD_CARD_KEY } from '../constants'; +import { AccountSelectContext } from '../context'; import { AccountSelectProps } from '../types'; -export const AccountSelectMobile: React.FC = ({ - isAddingNewCard, - options, - onChange, - ...restProps -}) => { - - - return ( - - {needExpiryDate && step >= 2 && ( +
- )} - {needCvv && step >= 3 && ( - - )} + {isShowExpiry && ( + + )} + {isShowCvv && ( + + )} +
); }, diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index 969a02b26e..0c3909fa83 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -36,6 +36,7 @@ export const AccountSelectDesktop = forwardRef { console.log(e); }} label={text('label', 'Элемент')} - fieldProps={{ leftAddons: }} + fieldProps={{ leftAddons: }} cardAddingProps={{ content: ( @@ -133,6 +133,7 @@ export const account_select_mobile: Story = { name: 'AccountSelectMobile', render: () => ( { content: ( - + @@ -105,7 +105,7 @@ render(() => { content: ( - + @@ -127,9 +127,9 @@ render(() => { ]; return ( }} + fieldProps={{ leftAddons: }} options={options} /> ); @@ -150,7 +150,7 @@ render(() => { { { return (
}} + fieldProps={{ leftAddons: }} options={options} cardAddingProps={{ content: ( { { { return (
}} + fieldProps={{ leftAddons: }} options={options} cardAddingProps={{ content: ( ( ( - { cardAddingProps, options, closeOnSelect = true, dataTestId, block = true, onChange, ...restProps }, - ref + { + cardAddingProps, + options, + closeOnSelect = true, + dataTestId, + block = true, + onChange, + size = 72, + ...restProps + }, + ref, ) => { const [error, setError] = useState(null); const { content, ...restCardAddingProps } = cardAddingProps ?? {}; @@ -46,6 +55,7 @@ export const AccountSelectMobile = forwardRef; + cardImage?: SingleProps; /** * Нужно ли отображать поле для ввода CVV From 5dd4e68eb3e9b20c4c089c826b4fc160f5d9f8b9 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 20 Oct 2025 11:47:35 +0300 Subject: [PATCH 22/27] feat(account-select): fix paddings in story description --- .../account-select/src/docs/description.mdx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/account-select/src/docs/description.mdx b/packages/account-select/src/docs/description.mdx index fd9b2f7a1c..8f34b6a772 100644 --- a/packages/account-select/src/docs/description.mdx +++ b/packages/account-select/src/docs/description.mdx @@ -6,7 +6,7 @@ render(() => { { key: '1', content: ( - + @@ -27,7 +27,7 @@ render(() => { { key: '2', content: ( - + @@ -48,7 +48,7 @@ render(() => { { key: '3', content: ( - + @@ -67,7 +67,7 @@ render(() => { ), }, ]; - return ; + return ; }); ``` @@ -79,7 +79,7 @@ render(() => { { key: '1', content: ( - + @@ -103,7 +103,7 @@ render(() => { { key: '2', content: ( - + @@ -127,7 +127,6 @@ render(() => { ]; return ( }} options={options} @@ -147,7 +146,7 @@ render(() => { { key: '1', content: ( - + { { key: '2', content: ( - + { return (
}} options={options} cardAddingProps={{ content: ( - + { { key: '1', content: ( - + { { key: '2', content: ( - + { options={options} cardAddingProps={{ content: ( - + Date: Thu, 23 Oct 2025 16:25:12 +0300 Subject: [PATCH 23/27] feat(account-select): added card icon resize and refactor docs --- .../src/Component.responsive.tsx | 4 +- .../components/custom-field/index.module.css | 3 + .../src/components/custom-field/index.tsx | 54 ++- .../multi-step-card-input/index.module.css | 6 +- .../multi-step-card-input/index.tsx | 11 +- packages/account-select/src/constants.ts | 9 + .../src/desktop/Component.desktop.tsx | 3 +- .../src/docs/Component.stories.tsx | 176 +++++--- .../account-select/src/docs/description.mdx | 410 +++++++++--------- .../src/mobile/Component.mobile.tsx | 1 + packages/account-select/src/types.ts | 3 +- 11 files changed, 372 insertions(+), 308 deletions(-) create mode 100644 packages/account-select/src/components/custom-field/index.module.css diff --git a/packages/account-select/src/Component.responsive.tsx b/packages/account-select/src/Component.responsive.tsx index e82030ea5b..e3cdbe3617 100644 --- a/packages/account-select/src/Component.responsive.tsx +++ b/packages/account-select/src/Component.responsive.tsx @@ -4,9 +4,9 @@ import { useIsDesktop } from '@alfalab/core-components-mq'; import { AccountSelectDesktop } from './desktop'; import { AccountSelectMobile } from './mobile'; -import type { AccountSelectResonsiveProps } from './types'; +import type { AccountSelectResponsiveProps } from './types'; -export const AccountSelectResponsive = forwardRef( +export const AccountSelectResponsive = forwardRef( ({ breakpoint, client, ...restProps }, ref) => { const isDesktop = useIsDesktop(breakpoint); diff --git a/packages/account-select/src/components/custom-field/index.module.css b/packages/account-select/src/components/custom-field/index.module.css new file mode 100644 index 0000000000..eedb68ed48 --- /dev/null +++ b/packages/account-select/src/components/custom-field/index.module.css @@ -0,0 +1,3 @@ +.leftAddon{ + padding-left: var(--gap-8); +} diff --git a/packages/account-select/src/components/custom-field/index.tsx b/packages/account-select/src/components/custom-field/index.tsx index d6c544acd3..bfa47eab3b 100644 --- a/packages/account-select/src/components/custom-field/index.tsx +++ b/packages/account-select/src/components/custom-field/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { FormControlDesktop } from '@alfalab/core-components-form-control/desktop'; import { FormControlMobile } from '@alfalab/core-components-form-control/mobile'; @@ -7,6 +7,8 @@ import { Field, FieldProps, OptionShape } from '@alfalab/core-components-select/ import { ADD_CARD_KEY } from '../../constants'; import { MultiStepCardInput } from '../multi-step-card-input'; +import styles from './index.module.css'; + export interface CustomFieldProps extends FieldProps { view?: 'desktop' | 'mobile'; } @@ -23,39 +25,49 @@ export const CustomField = ({ view = 'desktop', cardImage, leftAddons, + valueRenderer, + size, ...restProps }: CustomFieldProps) => { - const valueRenderer = useCallback( - ({ selected: selectedOption }: { selected?: OptionShape }) => { - if (selectedOption?.key === ADD_CARD_KEY) { - return ( - - ); - } - - return selectedOption?.content; - }, - [onSubmit, onInput, cardImage, needCvv, needExpiryDate, expiryAsDate], - ); + const fieldRenderer = ({ + selected: selectedOption, + selectedMultiple: selectedOptions, + }: { + selected?: OptionShape; + selectedMultiple: OptionShape[]; + }) => { + if (selectedOption?.key === ADD_CARD_KEY) { + return ( + + ); + } + + return valueRenderer && selected + ? valueRenderer({ selected: selectedOption, selectedMultiple: selectedOptions }) + : selectedOption?.content; + }; const FormControlComponent = view === 'mobile' ? FormControlMobile : FormControlDesktop; return ( ); }; diff --git a/packages/account-select/src/components/multi-step-card-input/index.module.css b/packages/account-select/src/components/multi-step-card-input/index.module.css index 9bce269b9e..68d92f1786 100644 --- a/packages/account-select/src/components/multi-step-card-input/index.module.css +++ b/packages/account-select/src/components/multi-step-card-input/index.module.css @@ -22,8 +22,12 @@ width: 19ch; } -.cardNumberInput:not(:focus) { +.cardNumberInputValid { width: 5ch; + + &:focus { + width: 19ch; + } } .expiryInput { diff --git a/packages/account-select/src/components/multi-step-card-input/index.tsx b/packages/account-select/src/components/multi-step-card-input/index.tsx index 75f8d57cc5..2baeba81a4 100644 --- a/packages/account-select/src/components/multi-step-card-input/index.tsx +++ b/packages/account-select/src/components/multi-step-card-input/index.tsx @@ -4,7 +4,7 @@ import cn from 'classnames'; import { ProductCover } from '@alfalab/core-components-product-cover'; -import { CARD_MASK, CVV_MASK, EXPIRY_MASK } from '../../constants'; +import { CARD_MASK, CVV_MASK, EXPIRY_MASK, PRODUCT_COVER_SIZE_MAPPER } from '../../constants'; import { useAccountSelectContext } from '../../context'; import { CardAddingProps, CardData } from '../../types'; import { formatCardNumber, getMaskedCardNumber } from '../../utils/formaters'; @@ -16,7 +16,7 @@ import styles from './index.module.css'; type MultiStepCardInputProps = Pick< CardAddingProps, 'onSubmit' | 'onInput' | 'cardImage' | 'needCvv' | 'needExpiryDate' | 'expiryAsDate' ->; +> & { size: 40 | 48 | 56 | 64 | 72 }; export const MultiStepCardInput: React.FC = memo( ({ @@ -26,6 +26,7 @@ export const MultiStepCardInput: React.FC = memo( needCvv = true, needExpiryDate = true, expiryAsDate = true, + size, }) => { const [step, setStep] = useState(1); const [cardNumber, setCardNumber] = useState(''); @@ -201,7 +202,7 @@ export const MultiStepCardInput: React.FC = memo( = 16 ? Number(cardNumber) : undefined} {...cardImage} - size={cardImage?.size ?? 48} + size={cardImage?.size ?? PRODUCT_COVER_SIZE_MAPPER[size]} />
= memo( onInput={handleCardNumberChange} onFocus={handleCardNumberFocus} onBlur={handleCardNumberBlur} - className={cn(styles.multistepInput, styles.cardNumberInput)} + className={cn(styles.multistepInput, styles.cardNumberInput, { + [styles.cardNumberInputValid]: validateCardNumber(cardNumber), + })} inputMode='numeric' pattern='[0-9]*' /> diff --git a/packages/account-select/src/constants.ts b/packages/account-select/src/constants.ts index cc98629d4f..fec7b9479d 100644 --- a/packages/account-select/src/constants.ts +++ b/packages/account-select/src/constants.ts @@ -1,7 +1,16 @@ import { MaskitoOptions } from '@maskito/core'; +import { Size } from '@alfalab/core-components-product-cover/typings'; export const ADD_CARD_KEY = '#ADD_NEW_CARD'; +export const PRODUCT_COVER_SIZE_MAPPER: Record = { + 40: 32, + 48: 32, + 56: 40, + 64: 40, + 72: 48, +}; + export const CARD_MASK: MaskitoOptions = { mask: [ /\d/, diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index 0c3909fa83..910694a974 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -83,9 +83,10 @@ export const AccountSelectDesktop = forwardRef diff --git a/packages/account-select/src/docs/Component.stories.tsx b/packages/account-select/src/docs/Component.stories.tsx index 7f9ee0ad20..a1060fa04b 100644 --- a/packages/account-select/src/docs/Component.stories.tsx +++ b/packages/account-select/src/docs/Component.stories.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; -import { boolean, text } from '@storybook/addon-knobs'; +import { boolean, select, text } from '@storybook/addon-knobs'; import { AccountSelectDesktop } from '../desktop'; import { PureCell } from '@alfalab/core-components-pure-cell'; import { ProductCover } from '@alfalab/core-components-product-cover'; @@ -9,6 +9,7 @@ import { AccountSelectMobile } from '../mobile'; import { CardData } from '../types'; import { Amount } from '@alfalab/core-components-amount'; import { Typography } from '@alfalab/core-components-typography'; +import { PRODUCT_COVER_SIZE_MAPPER } from '../constants'; const baseCard = { baseUrl: 'https://online.alfabank.ru/cards-images/cards/', @@ -24,9 +25,16 @@ const meta: Meta = { type Story = StoryObj; -const getOptions = (platform: 'desktop' | 'mobile' = 'desktop') => [ - { - key: '1', +const DATA = [ + { text: 'Карта с преимуществами', amount: 100000099, id: '1' }, + { text: 'Карта с кредитным лимитом', amount: 999999, id: '2' }, + { text: 'Карта с вашим дизайном', amount: 0, id: '3' }, +]; + +const getOptions = (platform: 'desktop' | 'mobile' = 'desktop') => + DATA.map((el) => ({ + key: el.id, + value: el, content: ( @@ -35,10 +43,10 @@ const getOptions = (platform: 'desktop' | 'mobile' = 'desktop') => [ - Карта с преимуществами + {el.text} [ ), - }, - { - key: '2', - content: ( - - - - - - - - Карта с кредитным лимитом - - - - - - ), - }, -]; + })); export const account_select_desktop: Story = { name: 'AccountSelectDesktop', render: () => { + const size = select('size', [40, 48, 56, 64, 72], 72); const [cardImage, setCardImage] = useState(undefined); const handleInput = (data: CardData) => { if (data.number.startsWith('111111')) { @@ -90,12 +74,41 @@ export const account_select_desktop: Story = { return ( { console.log(e); }} + valueRenderer={({ selected }) => { + return ( + + + + + + + + {selected?.value.text} + + + + + + ); + }} label={text('label', 'Элемент')} - fieldProps={{ leftAddons: }} + fieldProps={{ + leftAddons: ( + + ), + }} cardAddingProps={{ content: ( @@ -131,37 +144,70 @@ export const account_select_desktop: Story = { export const account_select_mobile: Story = { name: 'AccountSelectMobile', - render: () => ( - - - - - - - - Новая карта - - - - - ), - needCvv: boolean('needCvv', true), - needExpiryDate: boolean('needExpiryDate', true), - expiryAsDate: boolean('expiryAsDate', true), - }} - /> - ), + render: () => { + const size = select('size', [40, 48, 56, 64, 72], 72); + return ( + + ), + }} + valueRenderer={({ selected }) => { + return ( + + + + + + + + {selected?.value.text} + + + + + + ); + }} + cardAddingProps={{ + content: ( + + + + + + + + Новая карта + + + + + ), + needCvv: boolean('needCvv', true), + needExpiryDate: boolean('needExpiryDate', true), + expiryAsDate: boolean('expiryAsDate', true), + }} + /> + ); + }, }; export default meta; diff --git a/packages/account-select/src/docs/description.mdx b/packages/account-select/src/docs/description.mdx index 8f34b6a772..e710800c5e 100644 --- a/packages/account-select/src/docs/description.mdx +++ b/packages/account-select/src/docs/description.mdx @@ -2,72 +2,59 @@ ```jsx live render(() => { - const options = [ - { - key: '1', - content: ( - - - - - Экспресс-счёт ··1234 - - - - - - ), - }, - { - key: '2', - content: ( - - - - - Экспресс-счёт ··1234 - - - - - - ), - }, - { - key: '3', - content: ( - - + const DATA = [ + { text: 'Основной счёт ··1234', amount: 100000099, id: '1' }, + { text: 'Экспресс-счёт ··0909', amount: 100000099, id: '2' }, + { text: 'Депозитный счёт ··4567', amount: 0, id: '3' }, + ]; + const getOptions = DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + + + {el.text} + + + + + + ), + })); + + return ( + { + return ( + - - Экспресс-счёт ··4567 + + {selected?.value.text} - - - ), - }, - ]; - return ; + + ); + }} + label='Куда' + options={getOptions()} + /> + ); }); ``` @@ -75,61 +62,65 @@ render(() => { ```jsx live render(() => { - const options = [ - { - key: '1', - content: ( - - - - - - - - Карта с преимуществами - - - - - - ), - }, - { - key: '2', - content: ( - - - - - - - - Карта с кредитным лимитом - - - - - - ), - }, + const isDesktop = useIsDesktop(); + const DATA = [ + { text: 'Карта с преимуществами', amount: 100000099, id: '1' }, + { text: 'Карта с кредитным лимитом', amount: 999999, id: '2' }, + { text: 'Карта с вашим дизайном', amount: 0, id: '3' }, ]; + const getOptions = DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + + + + + + {el.text} + + + + + + ), + })); return ( }} - options={options} + valueRenderer={({ selected }) => { + return ( + + + + + + + + {selected?.value.text} + + + + + + ); + }} + fieldProps={{ leftAddons: }} + options={getOptions()} /> ); }); @@ -142,66 +133,41 @@ render(() => { ```jsx live render(() => { - const options = [ - { - key: '1', - content: ( - - - - - - - - Карта с преимуществами - - - - - - ), - }, - { - key: '2', - content: ( - - - - - - - - Карта с кредитным лимитом - - - - - - ), - }, + const isDesktop = useIsDesktop(); + const DATA = [ + { text: 'Карта с преимуществами', amount: 100000099, id: '1' }, + { text: 'Карта с кредитным лимитом', amount: 2500000, id: '2' }, ]; + const getOptions = DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + + + + + + {el.text} + + + + + + ), + })); const [cvv, setCvv] = React.useState(true); const [expiry, setExpiry] = React.useState(true); @@ -220,14 +186,37 @@ render(() => {
}} - options={options} + fieldProps={{ leftAddons: }} + options={getOptions()} + valueRenderer={({ selected }) => { + return ( + + + + + + + + {selected?.value.text} + + + + + + ); + }} cardAddingProps={{ content: ( { ```jsx live render(() => { - const options = [ - { - key: '1', - content: ( - - - - - - - - Карта с преимуществами - - - - - - ), - }, - { - key: '2', + const isDesktop = useIsDesktop(); + const DATA = [ + { text: 'Карта с преимуществами', amount: 100000099, id: '1' }, + { text: 'Карта с кредитным лимитом', amount: 100099, id: '2' }, + ]; + const getOptions = () => + DATA.map((el) => ({ + key: el.id, + value: el, content: ( { - Карта с кредитным лимитом + {el.text} { ), - }, - ]; + })); const [cardImage, setCardImage] = React.useState(undefined); const handleInput = (data) => { @@ -350,16 +314,38 @@ render(() => { return (
}} - options={options} + fieldProps={{ leftAddons: }} + options={getOptions()} + valueRenderer={({ selected }) => { + return ( + + + + + + + + {selected?.value.text} + + + + + + ); + }} cardAddingProps={{ content: ( diff --git a/packages/account-select/src/types.ts b/packages/account-select/src/types.ts index 2db4c9c7d6..9b213cd0b7 100644 --- a/packages/account-select/src/types.ts +++ b/packages/account-select/src/types.ts @@ -58,7 +58,6 @@ export interface AccountSelectProps | 'searchProps' | 'showSearch' | 'Search' - | 'valueRenderer' > { /** * Пропсы для добавления новой карты @@ -66,7 +65,7 @@ export interface AccountSelectProps cardAddingProps?: CardAddingProps; } -export interface AccountSelectResonsiveProps extends AccountSelectProps { +export interface AccountSelectResponsiveProps extends AccountSelectProps { /** * Контрольная точка, с нее начинается desktop версия * @default 1024 From 25c2dd3dcdae64f602d8b07f63c009bc920a584d Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 23 Oct 2025 16:48:46 +0300 Subject: [PATCH 24/27] feat(account-select): lint fixes --- packages/account-select/package.json | 8 ++++---- packages/account-select/src/Component.responsive.tsx | 2 +- .../src/components/custom-field/index.tsx | 2 +- .../src/components/multi-step-card-input/index.tsx | 12 +++++++----- packages/account-select/src/constants.ts | 5 +++-- .../account-select/src/desktop/Component.desktop.tsx | 8 ++++---- .../account-select/src/mobile/Component.mobile.tsx | 6 +++--- packages/account-select/src/types.ts | 4 ++-- .../account-select/src/utils/parse-date/index.ts | 2 +- tsconfig.react-docgen-typescript.json | 2 ++ 10 files changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/account-select/package.json b/packages/account-select/package.json index 952e0d17dd..df4ee8ecd8 100644 --- a/packages/account-select/package.json +++ b/packages/account-select/package.json @@ -18,13 +18,13 @@ "react-dom": "^16.9.0 || ^17.0.1 || ^18.0.0" }, "dependencies": { - "@alfalab/core-components-select": "^18.0.1", + "@alfalab/core-components-select": "^18.2.0", "@alfalab/core-components-popover": "^7.1.0", "@alfalab/core-components-form-control": "^13.0.1", - "@alfalab/core-components-product-cover": "^2.0.1", + "@alfalab/core-components-product-cover": "^2.0.2", "@alfalab/core-components-mq": "^5.0.1", - "@maskito/core": "^1.7.0", - "@maskito/react": "^1.7.0", + "@maskito/core": "^3.10.3", + "@maskito/react": "^3.10.3", "classnames": "^2.5.1", "tslib": "^2.4.0" }, diff --git a/packages/account-select/src/Component.responsive.tsx b/packages/account-select/src/Component.responsive.tsx index e3cdbe3617..e6e1613729 100644 --- a/packages/account-select/src/Component.responsive.tsx +++ b/packages/account-select/src/Component.responsive.tsx @@ -4,7 +4,7 @@ import { useIsDesktop } from '@alfalab/core-components-mq'; import { AccountSelectDesktop } from './desktop'; import { AccountSelectMobile } from './mobile'; -import type { AccountSelectResponsiveProps } from './types'; +import { type AccountSelectResponsiveProps } from './types'; export const AccountSelectResponsive = forwardRef( ({ breakpoint, client, ...restProps }, ref) => { diff --git a/packages/account-select/src/components/custom-field/index.tsx b/packages/account-select/src/components/custom-field/index.tsx index bfa47eab3b..176e025e9d 100644 --- a/packages/account-select/src/components/custom-field/index.tsx +++ b/packages/account-select/src/components/custom-field/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormControlDesktop } from '@alfalab/core-components-form-control/desktop'; import { FormControlMobile } from '@alfalab/core-components-form-control/mobile'; -import { Field, FieldProps, OptionShape } from '@alfalab/core-components-select/shared'; +import { Field, type FieldProps, type OptionShape } from '@alfalab/core-components-select/shared'; import { ADD_CARD_KEY } from '../../constants'; import { MultiStepCardInput } from '../multi-step-card-input'; diff --git a/packages/account-select/src/components/multi-step-card-input/index.tsx b/packages/account-select/src/components/multi-step-card-input/index.tsx index 2baeba81a4..d570d02aa3 100644 --- a/packages/account-select/src/components/multi-step-card-input/index.tsx +++ b/packages/account-select/src/components/multi-step-card-input/index.tsx @@ -6,7 +6,7 @@ import { ProductCover } from '@alfalab/core-components-product-cover'; import { CARD_MASK, CVV_MASK, EXPIRY_MASK, PRODUCT_COVER_SIZE_MAPPER } from '../../constants'; import { useAccountSelectContext } from '../../context'; -import { CardAddingProps, CardData } from '../../types'; +import { type CardAddingProps, type CardData } from '../../types'; import { formatCardNumber, getMaskedCardNumber } from '../../utils/formaters'; import { parseDate } from '../../utils/parse-date'; import { validateCardNumber, validateCvv, validateExpiry } from '../../utils/validate'; @@ -57,11 +57,13 @@ export const MultiStepCardInput: React.FC = memo( value, selection: [, to], } = state; + if (to >= 5 && !validateExpiry(value)) { setError('Введена неверная дата'); } else { setError(null); } + return state; }, ], @@ -71,7 +73,7 @@ export const MultiStepCardInput: React.FC = memo( const numberRefCallback = useCallback( (element: HTMLInputElement | null) => { - (numberRef as React.MutableRefObject).current = element; + numberRef.current = element; numberMaskRef(element); }, [numberMaskRef], @@ -79,7 +81,7 @@ export const MultiStepCardInput: React.FC = memo( const expiryRefCallback = useCallback( (element: HTMLInputElement | null) => { - (expiryRef as React.MutableRefObject).current = element; + expiryRef.current = element; expiryMaskRef(element); }, [expiryMaskRef], @@ -87,7 +89,7 @@ export const MultiStepCardInput: React.FC = memo( const cvvRefCallback = useCallback( (element: HTMLInputElement | null) => { - (cvvRef as React.MutableRefObject).current = element; + cvvRef.current = element; cvvMaskRef(element); }, [cvvMaskRef], @@ -166,7 +168,7 @@ export const MultiStepCardInput: React.FC = memo( expiryRef.current?.blur(); onSubmit?.({ number: cardNumber, - expiryDate: expiryAsDate ? parseDate(value as string) : value, + expiryDate: expiryAsDate ? parseDate(value) : value, }); } } diff --git a/packages/account-select/src/constants.ts b/packages/account-select/src/constants.ts index fec7b9479d..989ab2a08c 100644 --- a/packages/account-select/src/constants.ts +++ b/packages/account-select/src/constants.ts @@ -1,5 +1,6 @@ -import { MaskitoOptions } from '@maskito/core'; -import { Size } from '@alfalab/core-components-product-cover/typings'; +import { type MaskitoOptions } from '@maskito/core'; + +import { type Size } from '@alfalab/core-components-product-cover/typings'; export const ADD_CARD_KEY = '#ADD_NEW_CARD'; diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index 910694a974..a1668158ce 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -2,20 +2,20 @@ import React, { forwardRef, useMemo, useState } from 'react'; import { Popover } from '@alfalab/core-components-popover'; import { - AnyObject, + type AnyObject, Arrow, BaseOption, BaseSelect, - BaseSelectChangePayload, + type BaseSelectChangePayload, Optgroup as DefaultOptgroup, - OptionProps, + type OptionProps, OptionsList as DefaultOptionsList, } from '@alfalab/core-components-select/shared'; import { CustomField } from '../components/custom-field'; import { ADD_CARD_KEY } from '../constants'; import { AccountSelectContext } from '../context'; -import { AccountSelectProps } from '../types'; +import { type AccountSelectProps } from '../types'; import styles from './index.module.css'; diff --git a/packages/account-select/src/mobile/Component.mobile.tsx b/packages/account-select/src/mobile/Component.mobile.tsx index 6f4f465c53..d3fe136592 100644 --- a/packages/account-select/src/mobile/Component.mobile.tsx +++ b/packages/account-select/src/mobile/Component.mobile.tsx @@ -1,12 +1,12 @@ import React, { forwardRef, useMemo, useState } from 'react'; import { SelectMobile } from '@alfalab/core-components-select/mobile'; -import { BaseSelectChangePayload } from '@alfalab/core-components-select/shared'; +import { type BaseSelectChangePayload } from '@alfalab/core-components-select/shared'; -import { CustomField, CustomFieldProps } from '../components/custom-field'; +import { CustomField, type CustomFieldProps } from '../components/custom-field'; import { ADD_CARD_KEY } from '../constants'; import { AccountSelectContext } from '../context'; -import { AccountSelectProps } from '../types'; +import { type AccountSelectProps } from '../types'; const MobileCustomField = (props: CustomFieldProps) => ; diff --git a/packages/account-select/src/types.ts b/packages/account-select/src/types.ts index 9b213cd0b7..97e78926eb 100644 --- a/packages/account-select/src/types.ts +++ b/packages/account-select/src/types.ts @@ -1,5 +1,5 @@ -import { SingleProps } from '@alfalab/core-components-product-cover/typings'; -import { BaseSelectProps } from '@alfalab/core-components-select/shared'; +import { type SingleProps } from '@alfalab/core-components-product-cover/typings'; +import { type BaseSelectProps } from '@alfalab/core-components-select/shared'; export interface CardData { number: string; diff --git a/packages/account-select/src/utils/parse-date/index.ts b/packages/account-select/src/utils/parse-date/index.ts index 079da9cfbf..96fbce67f4 100644 --- a/packages/account-select/src/utils/parse-date/index.ts +++ b/packages/account-select/src/utils/parse-date/index.ts @@ -4,7 +4,7 @@ * @returns объект Date с последним днем указанного месяца и года */ export const parseDate = (expiryString: string): Date => { - if (!expiryString || !expiryString.includes('/')) { + if (!expiryString?.includes('/')) { throw new Error('Неверный формат даты. Ожидается формат "ММ/ГГ"'); } diff --git a/tsconfig.react-docgen-typescript.json b/tsconfig.react-docgen-typescript.json index 6015a0bf72..1f69f5c562 100644 --- a/tsconfig.react-docgen-typescript.json +++ b/tsconfig.react-docgen-typescript.json @@ -6,6 +6,8 @@ "paths": { "@alfalab/core-components-accordion": ["./packages/accordion/src"], "@alfalab/core-components-accordion/*": ["./packages/accordion/src/*"], + "@alfalab/core-components-account-select": ["./packages/account-select/src"], + "@alfalab/core-components-account-select/*": ["./packages/account-select/src/*"], "@alfalab/core-components-action-button": ["./packages/action-button/src"], "@alfalab/core-components-action-button/*": ["./packages/action-button/src/*"], "@alfalab/core-components-alert": ["./packages/alert/src"], From e13ef34dd546cd9d61996060015c50eba8713bdc Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 23 Oct 2025 17:41:46 +0300 Subject: [PATCH 25/27] feat(account-select): sort packages --- packages/account-select/package.json | 22 +++++++++---------- .../components/custom-field/index.module.css | 2 ++ .../src/desktop/Component.desktop.tsx | 4 ++-- .../account-select/src/docs/description.mdx | 16 +++++++------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/account-select/package.json b/packages/account-select/package.json index df4ee8ecd8..b4a49d9b85 100644 --- a/packages/account-select/package.json +++ b/packages/account-select/package.json @@ -9,25 +9,25 @@ ], "main": "index.js", "module": "./esm/index.js", - "publishConfig": { - "access": "public", - "directory": "dist" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.1 || ^18.0.0", - "react-dom": "^16.9.0 || ^17.0.1 || ^18.0.0" - }, "dependencies": { - "@alfalab/core-components-select": "^18.2.0", - "@alfalab/core-components-popover": "^7.1.0", "@alfalab/core-components-form-control": "^13.0.1", - "@alfalab/core-components-product-cover": "^2.0.2", "@alfalab/core-components-mq": "^5.0.1", + "@alfalab/core-components-popover": "^7.1.0", + "@alfalab/core-components-product-cover": "^2.0.2", + "@alfalab/core-components-select": "^18.2.0", "@maskito/core": "^3.10.3", "@maskito/react": "^3.10.3", "classnames": "^2.5.1", "tslib": "^2.4.0" }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.1 || ^18.0.0", + "react-dom": "^16.9.0 || ^17.0.1 || ^18.0.0" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, "themesVersion": "14.0.1", "varsVersion": "10.0.0" } diff --git a/packages/account-select/src/components/custom-field/index.module.css b/packages/account-select/src/components/custom-field/index.module.css index eedb68ed48..522b79a46a 100644 --- a/packages/account-select/src/components/custom-field/index.module.css +++ b/packages/account-select/src/components/custom-field/index.module.css @@ -1,3 +1,5 @@ +@import '@alfalab/core-components-vars/src/index.css'; + .leftAddon{ padding-left: var(--gap-8); } diff --git a/packages/account-select/src/desktop/Component.desktop.tsx b/packages/account-select/src/desktop/Component.desktop.tsx index a1668158ce..02ec03bc49 100644 --- a/packages/account-select/src/desktop/Component.desktop.tsx +++ b/packages/account-select/src/desktop/Component.desktop.tsx @@ -83,10 +83,10 @@ export const AccountSelectDesktop = forwardRef diff --git a/packages/account-select/src/docs/description.mdx b/packages/account-select/src/docs/description.mdx index e710800c5e..8baf3644dc 100644 --- a/packages/account-select/src/docs/description.mdx +++ b/packages/account-select/src/docs/description.mdx @@ -38,10 +38,10 @@ render(() => { - {selected?.value.text} + {selected.value.text} { - {selected?.value.text} + {selected.value.text} { - {selected?.value.text} + {selected.value.text} { - {selected?.value.text} + {selected.value.text} Date: Thu, 23 Oct 2025 18:41:57 +0300 Subject: [PATCH 26/27] feat(account-select): format code --- .../src/components/custom-field/index.module.css | 2 +- .../src/docs/Component.stories.tsx | 16 ++++------------ .../src/mobile/Component.mobile.tsx | 2 +- packages/account-select/src/types.ts | 7 +------ packages/account-select/tsconfig.json | 2 +- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/account-select/src/components/custom-field/index.module.css b/packages/account-select/src/components/custom-field/index.module.css index 522b79a46a..9c4a7a8988 100644 --- a/packages/account-select/src/components/custom-field/index.module.css +++ b/packages/account-select/src/components/custom-field/index.module.css @@ -1,5 +1,5 @@ @import '@alfalab/core-components-vars/src/index.css'; -.leftAddon{ +.leftAddon { padding-left: var(--gap-8); } diff --git a/packages/account-select/src/docs/Component.stories.tsx b/packages/account-select/src/docs/Component.stories.tsx index a1060fa04b..dbf9acfaed 100644 --- a/packages/account-select/src/docs/Component.stories.tsx +++ b/packages/account-select/src/docs/Component.stories.tsx @@ -82,9 +82,7 @@ export const account_select_desktop: Story = { return ( - + @@ -105,9 +103,7 @@ export const account_select_desktop: Story = { }} label={text('label', 'Элемент')} fieldProps={{ - leftAddons: ( - - ), + leftAddons: , }} cardAddingProps={{ content: ( @@ -152,17 +148,13 @@ export const account_select_mobile: Story = { label={text('label', 'Элемент')} options={getOptions('mobile')} fieldProps={{ - leftAddons: ( - - ), + leftAddons: , }} valueRenderer={({ selected }) => { return ( - + diff --git a/packages/account-select/src/mobile/Component.mobile.tsx b/packages/account-select/src/mobile/Component.mobile.tsx index d3fe136592..b774625bad 100644 --- a/packages/account-select/src/mobile/Component.mobile.tsx +++ b/packages/account-select/src/mobile/Component.mobile.tsx @@ -64,7 +64,7 @@ export const AccountSelectMobile = forwardRef diff --git a/packages/account-select/src/types.ts b/packages/account-select/src/types.ts index 97e78926eb..8cd1cb6a7b 100644 --- a/packages/account-select/src/types.ts +++ b/packages/account-select/src/types.ts @@ -52,12 +52,7 @@ export interface CardAddingProps { export interface AccountSelectProps extends Omit< BaseSelectProps, - | 'autocomplete' - | 'Field' - | 'nativeSelect' - | 'searchProps' - | 'showSearch' - | 'Search' + 'autocomplete' | 'Field' | 'nativeSelect' | 'searchProps' | 'showSearch' | 'Search' > { /** * Пропсы для добавления новой карты diff --git a/packages/account-select/tsconfig.json b/packages/account-select/tsconfig.json index 6ae852c007..062db2b50c 100644 --- a/packages/account-select/tsconfig.json +++ b/packages/account-select/tsconfig.json @@ -33,4 +33,4 @@ { "path": "../select/tsconfig.build.json" }, { "path": "../test-utils/tsconfig.build.json" } ] -} \ No newline at end of file +} From fccfb26b7803ab0ffa10b7c0e91bf1d71792f6f6 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 23 Oct 2025 18:45:50 +0300 Subject: [PATCH 27/27] feat(account-select): fix description --- .../account-select/src/docs/description.mdx | 169 ++++++++++-------- 1 file changed, 91 insertions(+), 78 deletions(-) diff --git a/packages/account-select/src/docs/description.mdx b/packages/account-select/src/docs/description.mdx index 8baf3644dc..c6b3dba6f1 100644 --- a/packages/account-select/src/docs/description.mdx +++ b/packages/account-select/src/docs/description.mdx @@ -7,28 +7,29 @@ render(() => { { text: 'Экспресс-счёт ··0909', amount: 100000099, id: '2' }, { text: 'Депозитный счёт ··4567', amount: 0, id: '3' }, ]; - const getOptions = DATA.map((el) => ({ - key: el.id, - value: el, - content: ( - - - - - {el.text} - - - - - - ), - })); + const getOptions = () => + DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + + + {el.text} + + + + + + ), + })); return ( { { text: 'Карта с кредитным лимитом', amount: 999999, id: '2' }, { text: 'Карта с вашим дизайном', amount: 0, id: '3' }, ]; - const getOptions = DATA.map((el) => ({ - key: el.id, - value: el, - content: ( - - - - - - - - {el.text} - - - - - - ), - })); + const getOptions = () => + DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + + + + + + {el.text} + + + + + + ), + })); return ( { { text: 'Карта с преимуществами', amount: 100000099, id: '1' }, { text: 'Карта с кредитным лимитом', amount: 2500000, id: '2' }, ]; - const getOptions = DATA.map((el) => ({ - key: el.id, - value: el, - content: ( - - - - - - - - {el.text} - - + DATA.map((el) => ({ + key: el.id, + value: el, + content: ( + + + - - - - ), - })); + + + + + {el.text} + + + + + + ), + })); const [cvv, setCvv] = React.useState(true); const [expiry, setExpiry] = React.useState(true); @@ -192,7 +195,12 @@ render(() => { return ( - + @@ -321,7 +329,12 @@ render(() => { return ( - +