diff --git a/src/components/Notification/Notification.scss b/src/components/Notification/Notification.scss index 4283a623..b5352cae 100644 --- a/src/components/Notification/Notification.scss +++ b/src/components/Notification/Notification.scss @@ -110,6 +110,10 @@ $notificationSourceIconSize: 36px; border-left: 4px solid var(--g-color-line-danger); } + &_active { + cursor: pointer; + } + &__swipe-wrap { width: 100%; overflow: hidden; @@ -151,7 +155,7 @@ $notificationSourceIconSize: 36px; background: var(--g-color-base-misc-light); } &__swipe-action_theme_base &__swipe-action-icon { - background: var(--g-color-text-misc-light); + background: var(--g-color-base-misc-heavy-hover); } &__swipe-action_theme_base &__swipe-action-text { color: var(--g-color-text-misc-heavy); @@ -186,4 +190,9 @@ $notificationSourceIconSize: 36px; &__swipe-action-text { font-size: 16px; } + + &__source-icon { + width: 36px; + height: 36px; + } } diff --git a/src/components/Notification/Notification.tsx b/src/components/Notification/Notification.tsx index 9a4f90fe..b8044377 100644 --- a/src/components/Notification/Notification.tsx +++ b/src/components/Notification/Notification.tsx @@ -17,7 +17,7 @@ export const Notification = React.memo(function Notification(props: Props) { const {notification} = props; const {title, content, formattedDate, source, unread, theme} = notification; - const modifiers: CnMods = {unread, theme, mobile}; + const modifiers: CnMods = {unread, theme, mobile, active: Boolean(notification.onClick)}; return (
( + const [position, setPosition] = React.useState<'left-action' | 'notification' | 'right-action'>( 'notification', ); @@ -122,7 +122,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p return () => { gesture.destroy(); }; - }, [position]); + }, [leftAction, position, rightAction, swipeThreshold]); return (
diff --git a/src/components/Notification/definitions.ts b/src/components/Notification/definitions.ts index 5b9b538a..c1099de5 100644 --- a/src/components/Notification/definitions.ts +++ b/src/components/Notification/definitions.ts @@ -25,6 +25,7 @@ export type NotificationProps = { title?: React.ReactNode; formattedDate?: React.ReactNode; unread?: boolean; + archived?: boolean; source?: NotificationSourceProps; theme?: NotificationTheme; className?: string; diff --git a/src/components/Notifications/NotificationWrapper.tsx b/src/components/Notifications/NotificationWrapper.tsx new file mode 100644 index 00000000..0adec7bc --- /dev/null +++ b/src/components/Notifications/NotificationWrapper.tsx @@ -0,0 +1,91 @@ +import React from 'react'; + +import {useMobile} from '@gravity-ui/uikit'; + +import {Notification} from '../Notification'; +import {NotificationWithSwipe} from '../Notification/NotificationWithSwipe'; +import {NotificationProps} from '../Notification/definitions'; +import {block} from '../utils/cn'; + +import './Notifications.scss'; + +const b = block('notifications'); + +export const NotificationWrapper = (props: { + notification: NotificationProps; + swipeThreshold?: number; +}) => { + const ref = React.useRef(null); + + const {notification, swipeThreshold} = props; + const [mobile] = useMobile(); + const [wrapperMaxHeight, setWrapperMaxHeight] = React.useState(undefined); + const [isRemoved, setIsRemoved] = React.useState(false); + + React.useEffect(() => { + if (!ref.current) { + if (!notification.archived && isRemoved) { + setIsRemoved(false); + } + return () => {}; + } + + if (notification.archived) { + const listener = (event: TransitionEvent) => { + if (event.propertyName === 'max-height') { + setIsRemoved(true); + ref.current?.removeEventListener('transitionend', listener); + } + }; + + ref.current.addEventListener('transitionend', listener); + + ref.current.style.transition = 'max-height 0.3s'; + setWrapperMaxHeight(0); + + return () => { + ref.current?.removeEventListener('transitionend', listener); + }; + } else { + setIsRemoved(false); + + setTimeout(() => { + if (!ref.current) return; + + ref.current.style.transition = 'none'; + ref.current.style.maxHeight = 'none'; + + const maxHeight = ref.current?.getBoundingClientRect().height ?? 0; + setWrapperMaxHeight(maxHeight); + }, 0); + + return () => {}; + } + }, [ref, notification.archived, isRemoved]); + + const style = wrapperMaxHeight === undefined ? {} : {maxHeight: `${wrapperMaxHeight}px`}; + + if (isRemoved) { + return null; + } + + return ( +
+ {mobile && notification.swipeActions ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/src/components/Notifications/Notifications.scss b/src/components/Notifications/Notifications.scss index ec09b915..70a69685 100644 --- a/src/components/Notifications/Notifications.scss +++ b/src/components/Notifications/Notifications.scss @@ -6,8 +6,8 @@ $block: '.#{variables.$ns}notifications'; display: flex; flex-direction: column; color: var(--g-color-text-primary); - background: var(--g-color-base-background); height: 100%; + width: 100%; &__head { display: flex; @@ -73,11 +73,15 @@ $block: '.#{variables.$ns}notifications'; &__notification-wrapper:hover:not(:first-child)::before, &__notification-wrapper:hover + &__notification-wrapper::before, // .unread - &__notification-wrapper.unread:not(:first-child)::before, - &__notification-wrapper.unread + &__notification-wrapper::before { + &__notification-wrapper_unread:not(:first-child)::before, + &__notification-wrapper_unread + &__notification-wrapper::before { content: ''; display: block; border-top: 1px solid transparent; margin: 0 12px; } + + &__notification-wrapper { + overflow-y: hidden; + } } diff --git a/src/components/Notifications/Notifications.tsx b/src/components/Notifications/Notifications.tsx index 9f8fc121..307c4b0e 100644 --- a/src/components/Notifications/Notifications.tsx +++ b/src/components/Notifications/Notifications.tsx @@ -1,9 +1,12 @@ import React from 'react'; +import {InfiniteScroll} from '../InfiniteScroll'; import {block} from '../utils/cn'; import {NotificationsEmptyState} from './NotificationsEmptyState'; +import {NotificationsErrorState} from './NotificationsErrorState'; import {NotificationsList} from './NotificationsList'; +import {NotificationsLoadingState} from './NotificationsLoadingState'; import {NotificationsProps} from './definitions'; import i18n from './i18n'; @@ -12,22 +15,41 @@ import './Notifications.scss'; const b = block('notifications'); export const Notifications = React.memo(function Notifications(props: NotificationsProps) { + let content: JSX.Element; + + const visibleNotificationsCount = props.notifications.filter((n) => !n.archived).length; + const hasUnloadedNotifications = + !props.areAllNotificationsLoaded && props.onLoadMoreNotifications; + + if (props.isLoading) { + content = ; + } else if (visibleNotificationsCount > 0 || hasUnloadedNotifications) { + content = ( + + + + ); + } else if (props.errorContent) { + content = ; + } else { + content = ; + } + return (
{props.title || i18n('title')}
{props.actions ?
{props.actions}
: null}
-
- {props.notifications.length > 0 ? ( - - ) : ( - - )} -
+
{content}
); }); + +async function noop() {} diff --git a/src/components/Notifications/NotificationsEmptyState.tsx b/src/components/Notifications/NotificationsEmptyState.tsx index d94bfa30..bdd35f31 100644 --- a/src/components/Notifications/NotificationsEmptyState.tsx +++ b/src/components/Notifications/NotificationsEmptyState.tsx @@ -4,6 +4,8 @@ import {Icon, useTheme} from '@gravity-ui/uikit'; import {block} from '../utils/cn'; +import i18n from './i18n/index'; + import './Notifications.scss'; const b = block('notifications'); @@ -12,16 +14,20 @@ const nothingFoundSvg = ``; -type Props = {content: React.ReactNode}; +type Props = {image?: React.ReactNode; content: React.ReactNode}; export const NotificationsEmptyState = React.memo(function NotificationsEmptyState(props: Props) { const theme = useTheme(); return (
- + {props.image ? ( + props.image + ) : ( + + )}
-
No notifications
+
{i18n('no-notifications')}
{props.content ? (
{props.content}
) : null} diff --git a/src/components/Notifications/NotificationsErrorState.tsx b/src/components/Notifications/NotificationsErrorState.tsx new file mode 100644 index 00000000..7fdd7e7a --- /dev/null +++ b/src/components/Notifications/NotificationsErrorState.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import {Icon, useTheme} from '@gravity-ui/uikit'; + +import {block} from '../utils/cn'; + +import i18n from './i18n/index'; + +import './Notifications.scss'; + +const b = block('notifications'); + +const errorSvg = ``; + +const errorDarkSvg = ``; + +type Props = {image?: React.ReactNode; content: React.ReactNode}; + +export const NotificationsErrorState = React.memo(function NotificationsEmptyState(props: Props) { + const theme = useTheme(); + + return ( +
+ {props.image ? ( + props.image + ) : ( + + )} +
+
{i18n('notifications-error')}
+ {props.content ? ( +
{props.content}
+ ) : null} +
+
+ ); +}); diff --git a/src/components/Notifications/NotificationsList.tsx b/src/components/Notifications/NotificationsList.tsx index 579dba20..1b8ce7c0 100644 --- a/src/components/Notifications/NotificationsList.tsx +++ b/src/components/Notifications/NotificationsList.tsx @@ -1,12 +1,10 @@ import React from 'react'; -import {useMobile} from '@gravity-ui/uikit'; - -import {Notification} from '../Notification'; -import {NotificationWithSwipe} from '../Notification/NotificationWithSwipe'; import {NotificationProps} from '../Notification/definitions'; import {block} from '../utils/cn'; +import {NotificationWrapper} from './NotificationWrapper'; + import './Notifications.scss'; const b = block('notifications'); @@ -16,30 +14,15 @@ type Props = { swipeThreshold?: number; }; -export const NotificationsList = React.memo(function NotificationsList({ - notifications, - swipeThreshold, -}: Props) { - const [mobile] = useMobile(); - +export const NotificationsList = React.memo(function NotificationsList(props: Props) { return (
- {notifications.map((notification) => ( -
( + - {mobile && notification.swipeActions ? ( - - ) : ( - - )} -
+ /> ))}
); diff --git a/src/components/Notifications/NotificationsLoadingState.tsx b/src/components/Notifications/NotificationsLoadingState.tsx new file mode 100644 index 00000000..2bd35768 --- /dev/null +++ b/src/components/Notifications/NotificationsLoadingState.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import {Loader} from '@gravity-ui/uikit'; + +import {block} from '../utils/cn'; + +import './Notifications.scss'; + +const b = block('notifications'); + +export const NotificationsLoadingState = React.memo(function NotificationsEmptyState() { + return ( +
+ +
+ ); +}); diff --git a/src/components/Notifications/NotificationsPopupWrapper.tsx b/src/components/Notifications/NotificationsPopupWrapper.tsx new file mode 100644 index 00000000..33d3f3ba --- /dev/null +++ b/src/components/Notifications/NotificationsPopupWrapper.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +const WIDTH = '320px'; +const HEIGHT = '470px'; + +type Props = React.PropsWithChildren<{ + className?: string; + style?: React.CSSProperties; + fullHeight?: boolean; +}>; + +export const NotificationsPopupWrapper = (props: Props) => { + const {className, style, fullHeight = true, children} = props; + + const finalStyles = React.useMemo((): React.CSSProperties => { + const heightStyles = fullHeight ? {height: HEIGHT} : {maxHeight: HEIGHT}; + return {...heightStyles, width: WIDTH, overflowY: 'auto', ...style}; + }, [fullHeight, style]); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/components/Notifications/README.md b/src/components/Notifications/README.md index 529e34b1..6faa8a5f 100644 --- a/src/components/Notifications/README.md +++ b/src/components/Notifications/README.md @@ -1,46 +1,61 @@ ## Notifications -Components for displaying notifications. +Components for displaying notifications ([storybook](https://preview.gravity-ui.com/components/?path=/story/components-notifications--default)). Can be used on desktop and touch devices. ### Simple usage example ```typescript const YourComponent: React.FC = () => { - const notifications = useMemo( + const notifications: NotificationProps = React.useMemo( () => [ { id: 'minimum', content: Bare minimum, + formattedDate: '13 seconds ago', }, ], [], ); - const action = useMemo(() => ({icon: Plus, text: 'Add', onClick: () => console.log('ADD')}), []); + const action = React.useMemo( + () => ({icon: Plus, text: 'Add', onClick: () => console.log('ADD')}), + [], + ); return ( - } - emptyMessage={'Unfortunately, there are no notifications for you, pal'} - /> + // If you use Notifications inside a popup, use NotificationsPopupWrapper + + } + emptyMessage={'Unfortunately, there are no notifications for you, pal'} + /> + ); }; ``` +For more code examples go to [Notifications.stories.tsx](https://github.com/gravity-ui/components/blob/main/src/components/Notifications/__stories__/Notifications.stories.tsx). + ### Components **Notifications** — renders notifications and actions on these notifications. -| Property | Type | Required | Default | Description | -| :--------------- | :-------------------- | :------: | :---------------- | :--------------------------------------------------------- | -| `notifications` | `NotificationProps[]` | `true` | `false` | Touch device (mobile) mode | -| `title` | `ReactNode` | | `"Notifications"` | Notifications' title | -| `actions` | `ReactNode` | | | Notifications' actions (e.g. create new, mark all as read) | -| `emptyMessage` | `ReactNode` | | | Message for «No notifications» case | -| `swipeThreshold` | `number` | | 0.4 | A value from 0 to 1 — the more the harder to swipe | +| Property | Type | Required | Default | Description | +| :-------------------------- | :-------------------- | :------: | :---------------- | :---------------------------------------------------------------------------------------- | +| `notifications` | `NotificationProps[]` | `true` | `false` | Touch device (mobile) mode | +| `title` | `ReactNode` | | `"Notifications"` | Notifications' title | +| `actions` | `ReactNode` | | | Notifications' actions (e.g. create new, mark all as read) | +| `areAllNotificationsLoaded` | `boolean` | | `false` | When `true` renders a Loader instead of the notifications | +| `onLoadMoreNotifications` | `() => Promise` | | `noop` | Callback is called when the user scrolls to the end (so you can fetch more notifications) | +| `isLoading` | `boolean` | | `false` | When `true` renders a Loader instead of the notifications | +| `errorContent` | `ReactNode` | | | Used for the Error state (the message under the «Error») | +| `errorImage` | `ReactNode` | | | Custom image for the Error state | +| `emptyContent` | `ReactNode` | | | Same as `errorContent`, but for the Empty state | +| `emptyImage` | `ReactNode` | | | Custom image for the Empty state | +| `swipeThreshold` | `number` | | 0.4 | A value from 0 to 1 — the more the harder it is to swipe | **Notification** — renders a notification with actions (side/bottom/swipe). @@ -50,21 +65,22 @@ const YourComponent: React.FC = () => { **NotificationProps** — notification's type: -| Property | Type | Required | Default | Description | -| :------------ | :---------------------------------- | :------: | :------ | :---------------------------------------------------- | -| id | `string` | `true` | | Unique identifier (used in `key` for example) | -| content | `ReactNode` | `true` | | Notification's content (what it's about) | -| title | `ReactNode` | | | Notification's title (bold) | -| formattedDate | `ReactNode` | | | Notification's creation date (already formatted) | -| unread | `boolean` | | `false` | Is notification unread | -| source | `NotificationSourceProps` | | | Notification's source (e.g. Cloud/Tracker/Console) | -| theme | `NotificationTheme` | | | Notification's theme (e.g. warning/danger) | -| className | `string` | | | Notification's `className` | -| sideActions | `ReactNode` | | | Notification's actions on the right side | -| bottomActions | `ReactNode` | | | Notification's bottom actions (as buttons by default) | -| swipeActions | `NotificationSwipeActionsProps` | | | Notification's action on left/right swipe | -| onMouseEnter | `MouseEventHandler` | | | Callback for `onMouseEnter` | -| onMouseLeave | `MouseEventHandler` | | | Callback for `onMouseLeave` | -| onClick | `MouseEventHandler` | | | Callback for `onClick` | +| Property | Type | Required | Default | Description | +| :------------ | :------------------------------ | :------: | :------ | :--------------------------------------------------------------- | +| id | `string` | `true` | | Unique identifier (used in `key` for example) | +| content | `ReactNode` | `true` | | Notification's content (what it's about) | +| title | `ReactNode` | | | Notification's title (bold) | +| formattedDate | `ReactNode` | | | Notification's creation date (already formatted) | +| unread | `boolean` | | `false` | Is notification unread | +| archived | `boolean` | | `false` | Is notification archived (invisible to the user) | +| source | `NotificationSourceProps` | | | Notification's source (e.g. Cloud/Tracker/Console) | +| theme | `NotificationTheme` | | | Notification's theme (e.g. warning/danger) | +| className | `string` | | | Notification's `className` | +| sideActions | `ReactNode` | | | Notification's actions on the right side | +| bottomActions | `ReactNode` | | | Notification's bottom actions (as buttons by default) | +| swipeActions | `NotificationSwipeActionsProps` | | | Notification's action on left/right swipe (mobile mode required) | +| onMouseEnter | `MouseEventHandler` | | | Callback for `onMouseEnter` | +| onMouseLeave | `MouseEventHandler` | | | Callback for `onMouseLeave` | +| onClick | `MouseEventHandler` | | | Callback for `onClick` | For a more detailed info on types go to [Notifications' types](https://github.com/gravity-ui/components/blob/main/src/components/Notifications/definitions.ts) and [Notification' types](https://github.com/gravity-ui/components/blob/main/src/components/Notification/definitions.ts). diff --git a/src/components/Notifications/__stories__/Notifications.stories.tsx b/src/components/Notifications/__stories__/Notifications.stories.tsx index 73158bc2..15e87e0b 100644 --- a/src/components/Notifications/__stories__/Notifications.stories.tsx +++ b/src/components/Notifications/__stories__/Notifications.stories.tsx @@ -1,39 +1,184 @@ +/* eslint-disable no-console */ import React from 'react'; -import {ComponentMeta, ComponentStory} from '@storybook/react'; +import {Bell} from '@gravity-ui/icons'; +import {Button, Icon, Popup} from '@gravity-ui/uikit'; +import {Meta, StoryFn} from '@storybook/react'; +import {delay} from '../../InfiniteScroll/__stories__/utils'; +import {NotificationProps} from '../../Notification/definitions'; import {Notifications} from '../Notifications'; +import {NotificationsPopupWrapper} from '../NotificationsPopupWrapper'; -import {mockNotifications, notificationsMockActions} from './mockData'; +import { + generateNotification, + mockNotifications, + notificationSideActions, + notificationsMockActions, +} from './mockData'; export default { title: 'Components/Notifications', component: Notifications, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( -
- -
-); - -export const Default = Template.bind({}); -Default.args = { - notifications: mockNotifications, - actions: notificationsMockActions, -}; - -export const Empty = Template.bind({}); -Empty.args = { - notifications: [], - emptyMessage: 'You have not received any notifications', +} as Meta; + +const wrapperStyles = { + borderRadius: '8px', + border: '1px solid var(--g-color-line-generic)', + background: 'var(--g-color-base-background)', + margin: '4px', +}; + +const Wrapper = (props: React.PropsWithChildren) => { + return ( + + {props.children} + + ); +}; + +type BooleanMap = Record; + +export const Default: StoryFn = () => { + const {notifications, actions} = useNotificationsWithActions(); + + return ( + + + + ); }; + +export const LoadByScrolling: StoryFn = () => { + const [notifications, setNotifications] = React.useState([]); + const areAllNotificationsLoaded = notifications.length >= 40; + + const onLoadMoreNotifications = async () => { + await delay(1500); + + const newNotifications = Array.from({length: 10}).map((_, i) => + generateNotification(1 + notifications.length + i), + ); + + setNotifications((value) => [...value, ...newNotifications]); + }; + + return ( + + + + ); +}; + +export const InsideAPopup: StoryFn = () => { + const {notifications, actions} = useNotificationsWithActions(); + const [isOpen, setIsOpen] = React.useState(false); + const ref = React.useRef(null); + + return ( + <> + + + + + + + + ); +}; + +export const Loading: StoryFn = () => { + return ( + + + + ); +}; + +export const Error: StoryFn = () => { + return ( + + +
Some error occurred
+
+ +
+ + } + /> +
+ ); +}; + +export const Empty: StoryFn = () => { + return ( + + + + ); +}; + +function useNotificationsWithActions() { + const [unreadNotifications, setUnreadNotifications] = React.useState({ + tracker: true, + samurai: true, + }); + + const [archivedNotifications, setArchivedNotifications] = React.useState({}); + + const getSideActions = React.useCallback( + ( + id: NotificationProps['id'], + unread: boolean | undefined, + archived: boolean | undefined, + ) => ( + <> + {notificationSideActions.read(Boolean(unread), () => + setUnreadNotifications((current) => ({...current, [id]: !unread})), + )} + {notificationSideActions.archive(() => + setArchivedNotifications((current) => ({...current, [id]: !archived})), + )} + + ), + [], + ); + + const notifications = React.useMemo( + () => + mockNotifications.map((notification: NotificationProps) => { + const id = notification.id; + const unread = unreadNotifications[id]; + const archived = archivedNotifications[id]; + + return { + ...notification, + unread, + archived, + sideActions: getSideActions(id, unread, archived), + }; + }), + [unreadNotifications, archivedNotifications, getSideActions], + ); + + const actions = ( + <> + {notificationsMockActions.unarchive(() => setArchivedNotifications({}))} + {notificationsMockActions.filter()} + + ); + + return {notifications, actions}; +} diff --git a/src/components/Notifications/__stories__/mockData.tsx b/src/components/Notifications/__stories__/mockData.tsx index 657f8b6f..f7d5e872 100644 --- a/src/components/Notifications/__stories__/mockData.tsx +++ b/src/components/Notifications/__stories__/mockData.tsx @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import React from 'react'; -import {Archive, Funnel, PencilToSquare, Plus, TrashBin} from '@gravity-ui/icons'; -import {DropdownMenu} from '@gravity-ui/uikit'; +import {Archive, ArrowRotateLeft, CircleCheck, Funnel, TrashBin} from '@gravity-ui/icons'; +import {DropdownMenu, Link} from '@gravity-ui/uikit'; import {NotificationAction} from '../../Notification/NotificationAction'; import {NotificationSwipeAction} from '../../Notification/NotificationSwipeAction'; @@ -13,11 +13,16 @@ import { svgReactStoryIcon, svgTrackerStoryIcon, svgYandexStoryIcon, + trackerUserIcon, } from './storyIcons'; -export const notificationsMockActions: JSX.Element = ( - <> - console.log('ADD')}} /> +const LINK = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; + +export const notificationsMockActions = { + unarchive: (onClick: () => void) => ( + + ), + filter: () => ( console.log('cloud')}, ]} /> - -); + ), +}; export const notificationsMockSwipeActions: NotificationSwipeActionsProps = { left: { @@ -59,16 +64,20 @@ export const notificationsMockSwipeActions: NotificationSwipeActionsProps = { }, }; -export const notificationSideActions: JSX.Element = ( - <> - console.log('FILTER')}} - /> +export const notificationSideActions = { + read: (unread: boolean, onClick: () => void) => ( console.log('DELETE')}} + action={{ + icon: unread ? CircleCheck : ArrowRotateLeft, + text: `Mark as ${unread ? 'read' : 'unread'}`, + onClick, + }} /> - -); + ), + archive: (onClick: () => void) => ( + + ), +}; export const notificationBottomActions: JSX.Element = ( <> @@ -85,27 +94,44 @@ export const mockNotifications: NotificationProps[] = [ { id: 'tracker', title: 'An unread notification', - content: 'No one has read this notification yet...', + content: ( +
+ Shrek desperately wants your attention in this{' '} + + ticket + + +
+ ), formattedDate: 'just now', source: { title: 'Tracker', icon: svgTrackerStoryIcon, - href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + href: LINK, }, - unread: true, - sideActions: notificationSideActions, swipeActions: notificationsMockSwipeActions, }, { id: 'samurai', content: A samurai has no goal, only a path, formattedDate: '12 seconds ago', - unread: true, swipeActions: notificationsMockSwipeActions, }, { id: 'minimum', content: Bare minimum, + formattedDate: '13 seconds ago', }, { id: 'ninja', @@ -124,11 +150,9 @@ export const mockNotifications: NotificationProps[] = [ source: { title: 'Yandex', icon: svgYandexStoryIcon, - href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + href: LINK, }, - unread: false, theme: 'info', - sideActions: notificationSideActions, swipeActions: notificationsMockSwipeActions, }, { @@ -140,7 +164,7 @@ export const mockNotifications: NotificationProps[] = [ source: { title: 'Billing', icon: svgCloudStoryIcon, - href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + href: LINK, }, theme: 'success', bottomActions: notificationBottomActions, @@ -153,10 +177,19 @@ export const mockNotifications: NotificationProps[] = [ source: { title: 'React', icon: svgReactStoryIcon, - href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + href: LINK, }, formattedDate: 'ethernity ago', theme: 'danger', swipeActions: {...notificationsMockSwipeActions, right: undefined}, }, ]; + +export function generateNotification(index: number): NotificationProps { + return { + id: `notification-${index}`, + title: `We are number ${index}`, + content: `You're viewing notification #${index}`, + formattedDate: `${Math.round(index / 2) + 2} minutes ago`, + }; +} diff --git a/src/components/Notifications/__stories__/storyIcons.ts b/src/components/Notifications/__stories__/storyIcons.ts index fc424e5a..7e5a5a04 100644 --- a/src/components/Notifications/__stories__/storyIcons.ts +++ b/src/components/Notifications/__stories__/storyIcons.ts @@ -2,3 +2,4 @@ export const svgCloudStoryIcon = ``; export const svgTrackerStoryIcon = ``; export const svgYandexStoryIcon = ``; +export const trackerUserIcon = `data:image/png;base64,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`; diff --git a/src/components/Notifications/definitions.ts b/src/components/Notifications/definitions.ts index dd025413..13d0402b 100644 --- a/src/components/Notifications/definitions.ts +++ b/src/components/Notifications/definitions.ts @@ -7,7 +7,16 @@ export type NotificationsProps = { actions?: React.ReactNode; notifications: NotificationProps[]; + areAllNotificationsLoaded?: boolean; + onLoadMoreNotifications?: () => Promise; + + isLoading?: boolean; + + errorContent?: React.ReactNode; + errorImage?: React.ReactNode; + + emptyContent?: React.ReactNode; + emptyImage?: React.ReactNode; - emptyMessage?: React.ReactNode; swipeThreshold?: number; }; diff --git a/src/components/Notifications/i18n/en.json b/src/components/Notifications/i18n/en.json index 64586638..6a3ae547 100644 --- a/src/components/Notifications/i18n/en.json +++ b/src/components/Notifications/i18n/en.json @@ -1,3 +1,5 @@ { - "title": "Notifications" + "title": "Notifications", + "notifications-error": "Error", + "no-notifications": "No notifications" } diff --git a/src/components/Notifications/i18n/ru.json b/src/components/Notifications/i18n/ru.json index bc77c487..b789c20b 100644 --- a/src/components/Notifications/i18n/ru.json +++ b/src/components/Notifications/i18n/ru.json @@ -1,3 +1,5 @@ { - "title": "Уведомления" + "title": "Уведомления", + "notifications-error": "Ошибка", + "no-notifications": "Нет уведомлений" } diff --git a/src/components/Notifications/index.ts b/src/components/Notifications/index.ts index e2188db5..c85574ee 100644 --- a/src/components/Notifications/index.ts +++ b/src/components/Notifications/index.ts @@ -1,2 +1,3 @@ -export * from './definitions'; export * from './Notifications'; +export * from './NotificationsPopupWrapper'; +export * from './definitions';