feat(TEAMMSBMOB-14876): виджет быстрые действия
@@ -0,0 +1,99 @@
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import type { ExpandableProps } from './types';
|
||||
|
||||
const HEIGHT = 92;
|
||||
const ANIMATION_DURATION = 0.3;
|
||||
|
||||
const expandAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
height: ${HEIGHT}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const collapseAnimation = keyframes`
|
||||
from {
|
||||
opacity: 1;
|
||||
height: ${HEIGHT}px;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Actions = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
padding: 8px 4px 8px 16px;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ActionsBox = styled.div<ExpandableProps>`
|
||||
display: grid;
|
||||
width: 100%;
|
||||
row-gap: ${({ $isExpanded }) => ($isExpanded ? '32px' : '0')};
|
||||
transition: row-gap ${ANIMATION_DURATION}s ease-in-out;
|
||||
`;
|
||||
|
||||
export const ActionSection = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 108px);
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
max-width: 572px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ActionSectionExpanded = styled.div<ExpandableProps>`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 108px);
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
max-width: 572px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
animation: ${({ $isExpanded }) => ($isExpanded ? expandAnimation : collapseAnimation)} ${ANIMATION_DURATION}s ease forwards;
|
||||
`;
|
||||
|
||||
export const ActionsItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: ${HEIGHT}px;
|
||||
max-width: 98px;
|
||||
`;
|
||||
|
||||
export const Icon = styled.img`
|
||||
object-fit: cover;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`;
|
||||
|
||||
export const IconBox = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--bg-primary);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 8px;
|
||||
transition: box-shadow ${ANIMATION_DURATION}s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 16px 0px #1e2e611a;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonBox = styled.div<ExpandableProps>`
|
||||
svg {
|
||||
transition: transform ${ANIMATION_DURATION}s ease;
|
||||
transform: ${({ $isExpanded }) => ($isExpanded ? 'rotate(180deg)' : 'rotate(0)')};
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { ButtonIcon } from '@fractal-ui/core';
|
||||
import { DownIcon } from '@fractal-ui/library';
|
||||
import { Text } from '@fractal-ui/styling';
|
||||
|
||||
import * as S from './QuickActions.styles';
|
||||
import { ACTIONS, MAX_VISIBLE_ITEMS } from './constants';
|
||||
import type { ActionItem } from './types';
|
||||
|
||||
const QuickActions = (): ReactElement => {
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
const [showHiddenContent, setShowHiddenContent] = useState(false);
|
||||
|
||||
const toggleClick = useCallback(() => {
|
||||
if (isExpanded) {
|
||||
setExpanded(false);
|
||||
} else {
|
||||
setExpanded(true);
|
||||
setShowHiddenContent(true);
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
const handleAnimationEnd = useCallback(() => {
|
||||
if (!isExpanded) {
|
||||
setShowHiddenContent(false);
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
const getActionItem = (action: ActionItem[]): JSX.Element[] =>
|
||||
action.map(({ name, icon }) => (
|
||||
<S.ActionsItem
|
||||
key={name?.toString()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
// TODO: поведение при клике на элементе
|
||||
}}
|
||||
>
|
||||
<S.IconBox>
|
||||
<S.Icon alt="icon" src={icon} />
|
||||
</S.IconBox>
|
||||
<Text.P3Short as="span">{name}</Text.P3Short>
|
||||
</S.ActionsItem>
|
||||
));
|
||||
|
||||
const visibleActions = ACTIONS.slice(0, MAX_VISIBLE_ITEMS);
|
||||
const hiddenActions = ACTIONS.slice(MAX_VISIBLE_ITEMS);
|
||||
|
||||
return (
|
||||
<S.Actions>
|
||||
<S.ActionsBox $isExpanded={isExpanded}>
|
||||
<S.ActionSection>{getActionItem(visibleActions)}</S.ActionSection>
|
||||
|
||||
{showHiddenContent && (
|
||||
<S.ActionSectionExpanded $isExpanded={isExpanded} onAnimationEnd={handleAnimationEnd}>
|
||||
{getActionItem(hiddenActions)}
|
||||
</S.ActionSectionExpanded>
|
||||
)}
|
||||
</S.ActionsBox>
|
||||
<S.ButtonBox $isExpanded={isExpanded}>
|
||||
<ButtonIcon
|
||||
dataAction="toggle"
|
||||
icon={DownIcon}
|
||||
iconColor="text.secondary"
|
||||
shape="default"
|
||||
size="M"
|
||||
variant="secondary"
|
||||
onClick={toggleClick}
|
||||
/>
|
||||
</S.ButtonBox>
|
||||
</S.Actions>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickActions;
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
@@ -0,0 +1,25 @@
|
||||
import icon1 from './01.png';
|
||||
import icon2 from './02.png';
|
||||
import icon3 from './03.png';
|
||||
import icon4 from './04.png';
|
||||
import icon5 from './05.png';
|
||||
import icon6 from './06.png';
|
||||
import icon7 from './07.png';
|
||||
import icon8 from './08.png';
|
||||
import icon9 from './09.png';
|
||||
import icon10 from './10.png';
|
||||
|
||||
export const Icons = {
|
||||
payment: icon1,
|
||||
statement: icon2,
|
||||
deposit: icon3,
|
||||
application: icon4,
|
||||
cash: icon5,
|
||||
phoneTransfer: icon6,
|
||||
qrPayment: icon7,
|
||||
signPayment: icon8,
|
||||
certificate: icon9,
|
||||
offices: icon10,
|
||||
} as const;
|
||||
|
||||
export type IconName = keyof typeof Icons;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Icons } from './assets';
|
||||
import type { ActionItem } from './types';
|
||||
|
||||
export const ACTIONS: ActionItem[] = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
Перевести
|
||||
<br />и оплатить
|
||||
</>
|
||||
),
|
||||
icon: Icons.payment,
|
||||
},
|
||||
{ name: 'Заказать выписку', icon: Icons.statement },
|
||||
{ name: 'Открыть депозит', icon: Icons.deposit },
|
||||
{ name: 'Создать заявку', icon: Icons.application },
|
||||
{ name: 'Заказать наличные', icon: Icons.cash },
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
Перевести
|
||||
<br />
|
||||
по телефону
|
||||
</>
|
||||
),
|
||||
icon: Icons.phoneTransfer,
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
Оплатить
|
||||
<br />
|
||||
по QR-коду
|
||||
</>
|
||||
),
|
||||
icon: Icons.qrPayment,
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
Платежи
|
||||
<br />
|
||||
на подпись
|
||||
</>
|
||||
),
|
||||
icon: Icons.signPayment,
|
||||
},
|
||||
{ name: 'Оформить справку', icon: Icons.certificate },
|
||||
{ name: 'Офисы и банкоматы', icon: Icons.offices },
|
||||
];
|
||||
|
||||
export const MAX_VISIBLE_ITEMS = 5;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './QuickActions';
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface ActionItem {
|
||||
name: ReactNode | string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface ExpandableProps {
|
||||
$isExpanded: boolean;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as QuickActions } from './QuickActions';
|
||||