-
Notifications
You must be signed in to change notification settings - Fork 21
PM-2133 dismissable banner #1261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
419163e
6f9bb56
1fc2ecd
7b7d7b7
a6d033a
53baf43
8f36483
b9fe4ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ import { TableLoading } from '~/apps/admin/src/lib' | |
| import { handleError } from '~/apps/admin/src/lib/utils' | ||
| import { EnvironmentConfig } from '~/config' | ||
| import { BaseModal, Button, InputCheckbox, InputText } from '~/libs/ui' | ||
| import { NotificationContextType, useNotification } from '~/libs/shared' | ||
|
|
||
| import { | ||
| useFetchScreeningReview, | ||
|
|
@@ -226,6 +227,7 @@ const computePhaseCompletionFromScreenings = ( | |
|
|
||
| // eslint-disable-next-line complexity | ||
| export const ChallengeDetailsPage: FC<Props> = (props: Props) => { | ||
| const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const [searchParams, setSearchParams] = useSearchParams() | ||
| const location = useLocation() | ||
| const navigate = useNavigate() | ||
|
|
@@ -1323,6 +1325,16 @@ export const ChallengeDetailsPage: FC<Props> = (props: Props) => { | |
| : undefined | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const shouldShowChallengeMetaRow = Boolean(statusLabel) || trackTypePills.length > 0 | ||
|
|
||
| useEffect(() => { | ||
| const notification = showBannerNotification({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| id: 'ai-review-scores-warning', | ||
| message: `AI Review Scores are advisory only to provide immediate, | ||
| educational, and actionable feedback to members. | ||
| AI Review Scores are not influence winner selection.`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message text contains a grammatical error. It should be 'AI Review Scores do not influence winner selection.' instead of 'AI Review Scores are not influence winner selection.' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [❗❗ |
||
| }) | ||
| return () => notification && removeNotification(notification.id) | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, [showBannerNotification]) | ||
|
|
||
| return ( | ||
| <PageWrapper | ||
| pageTitle={challengeInfo?.name ?? ''} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| export * from './profile-context-data.model' | ||
| export { default as profileContext, defaultProfileContextData } from './profile.context' | ||
| export { default as profileContext, defaultProfileContextData, useProfileContext } from './profile.context' | ||
| export * from './profile.context-provider' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { FC } from 'react' | ||
|
|
||
| import { Notification } from '~/libs/ui' | ||
|
|
||
| import { NotificationContextType, useNotification } from './Notifications.context' | ||
| import styles from './NotificationsContainer.module.scss' | ||
|
|
||
| const NotificationsContainer: FC = () => { | ||
| const { notifications, removeNotification }: NotificationContextType = useNotification() | ||
|
|
||
| return ( | ||
| <div className={styles.wrap}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| {notifications.map(n => ( | ||
| <Notification key={n.id} notification={n} onClose={removeNotification} /> | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ))} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default NotificationsContainer | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react' | ||
|
|
||
| import { useProfileContext } from '~/libs/core' | ||
|
|
||
| import { dismiss, wasDismissed } from './localstorage.utils' | ||
|
|
||
| export type NotificationType = 'success' | 'error' | 'info' | 'warning' | 'banner'; | ||
|
|
||
| export interface Notification { | ||
| id: string; | ||
| type: NotificationType; | ||
| icon?: ReactNode | ||
| message: string; | ||
| duration?: number; // in ms | ||
| } | ||
|
|
||
| type NotifyPayload = string | (Partial<Notification> & { message: string }) | ||
|
|
||
| export interface NotificationContextType { | ||
| notifications: Notification[]; | ||
| notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void; | ||
| showBannerNotification: (message: NotifyPayload) => Notification | void; | ||
| removeNotification: (id: string) => void; | ||
| } | ||
|
|
||
| const NotificationContext = createContext<NotificationContextType | undefined>(undefined) | ||
|
|
||
| export const useNotification = (): NotificationContextType => { | ||
| const context = useContext(NotificationContext) | ||
| if (!context) throw new Error('useNotification must be used within a NotificationProvider') | ||
| return context | ||
| } | ||
|
|
||
| export const NotificationProvider: React.FC<{ | ||
| children: ReactNode, | ||
| }> = props => { | ||
| const profileCtx = useProfileContext() | ||
| const uuid = profileCtx.profile?.userId ?? 'annon' | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const [notifications, setNotifications] = useState<Notification[]>([]) | ||
|
|
||
| const removeNotification = useCallback((id: string, persist?: boolean) => { | ||
| setNotifications(prev => prev.filter(n => n.id !== id)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| if (persist) { | ||
| dismiss(id) | ||
| } | ||
| }, []) | ||
|
|
||
| const notify = useCallback( | ||
| (message: NotifyPayload, type: NotificationType = 'info', duration = 3000) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| const id = `${uuid}[${typeof message === 'string' ? message : message.id}]` | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const newNotification: Notification | ||
| = typeof message === 'string' | ||
| ? { duration, id, message, type } | ||
| : { duration, type, ...message, id } | ||
|
|
||
| if (wasDismissed(id)) { | ||
| return undefined | ||
| } | ||
|
|
||
| setNotifications(prev => [...prev, newNotification]) | ||
|
|
||
| if (duration > 0) { | ||
| setTimeout(() => removeNotification(id), duration) | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return newNotification | ||
| }, | ||
| [uuid], | ||
| ) | ||
|
|
||
| const showBannerNotification = useCallback(( | ||
| message: NotifyPayload, | ||
| ) => notify(message, 'banner', 0), [notify]) | ||
|
|
||
| const ctxValue = useMemo(() => ({ | ||
| notifications, | ||
| notify, | ||
| removeNotification, | ||
| showBannerNotification, | ||
| }), [ | ||
| notifications, | ||
| notify, | ||
| removeNotification, | ||
| showBannerNotification, | ||
| ]) | ||
|
|
||
| return ( | ||
| <NotificationContext.Provider value={ctxValue}> | ||
| {props.children} | ||
| </NotificationContext.Provider> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| @import "@libs/ui/styles/includes"; | ||
|
|
||
| .wrap { | ||
| position: relative; | ||
| width: 100%; | ||
| z-index: 1000; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { default as NotificationsContainer } from './Notifications.container' | ||
| export * from './Notifications.context' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const lsKeyPrefix = 'notificationDismissed' | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
|
|
||
| export const wasDismissed = (id: string): boolean => ( | ||
| (localStorage.getItem(`${lsKeyPrefix}[${id}]`)) !== null | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| ) | ||
|
|
||
| export const dismiss = (id: string): void => { | ||
| localStorage.setItem(`${lsKeyPrefix}[${id}]`, JSON.stringify(true)) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { FC, ReactNode, useCallback } from 'react' | ||
|
|
||
| import { NotificationBanner } from './banner' | ||
|
|
||
| interface NotificationProps { | ||
| notification: { | ||
| icon?: ReactNode; | ||
| id: string; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| message: string; | ||
| type: string; | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| onClose: (id: string, save?: boolean) => void | ||
| } | ||
|
|
||
| const Notification: FC<NotificationProps> = props => { | ||
| const handleClose = useCallback((save?: boolean) => { | ||
| props.onClose(props.notification.id, save) | ||
| }, [props.onClose, props.notification.id]) | ||
|
|
||
| if (props.notification.type === 'banner') { | ||
| return ( | ||
| <NotificationBanner | ||
| icon={props.notification.icon} | ||
| content={props.notification.message} | ||
| onClose={handleClose} | ||
| /> | ||
| ) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
|
|
||
| return <></> | ||
| } | ||
|
|
||
| export default Notification | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| @import '../../../styles/includes'; | ||
|
|
||
| .wrap { | ||
| background: #60267D; | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| color: $tc-white; | ||
|
|
||
| font-family: "Nunito Sans", sans-serif; | ||
| font-size: 14px; | ||
| line-height: 20px; | ||
|
|
||
| .inner { | ||
| max-width: $xxl-min; | ||
| padding: $sp-2 0; | ||
| @include pagePaddings; | ||
| margin: 0 auto; | ||
| width: 100%; | ||
| display: flex; | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| } | ||
| } | ||
|
|
||
| .close { | ||
| cursor: pointer; | ||
| color: $tc-white; | ||
| flex: 0 0; | ||
| margin-left: auto; | ||
| border-radius: 50%; | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| border: 2px solid white; | ||
| @include ltemd { | ||
| margin-left: $sp-3; | ||
| } | ||
| } | ||
|
|
||
| .icon { | ||
| flex: 0 0; | ||
| margin-right: $sp-2; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { Meta, StoryObj } from '@storybook/react' | ||
|
|
||
| import NotificationBanner from './NotificationBanner' | ||
|
|
||
| const meta: Meta<typeof NotificationBanner> = { | ||
| argTypes: { | ||
| content: { | ||
| description: 'Content displayed inside the notification banner', | ||
| }, | ||
| persistent: { | ||
| defaultValue: false, | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| description: 'Set to true to hide the close icon button', | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| }, | ||
| component: NotificationBanner, | ||
| excludeStories: /.*Decorator$/, | ||
| tags: ['autodocs'], | ||
| title: 'Components/NotificationBanner', | ||
| } | ||
|
|
||
| export default meta | ||
|
|
||
| type Story = StoryObj<typeof NotificationBanner>; | ||
|
|
||
| export const Primary: Story = { | ||
| args: { | ||
| content: 'Help tooltip', | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { FC, ReactNode, useCallback } from 'react' | ||
|
|
||
| import { InformationCircleIcon } from '@heroicons/react/outline' | ||
|
|
||
| import { IconOutline } from '../../svgs' | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import styles from './NotificationBanner.module.scss' | ||
|
|
||
| interface NotificationBannerProps { | ||
| persistent?: boolean | ||
| content: ReactNode | ||
| icon?: ReactNode | ||
| onClose?: (save?: boolean) => void | ||
| } | ||
|
|
||
| const NotificationBanner: FC<NotificationBannerProps> = props => { | ||
| const handleClose = useCallback(() => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| props.onClose?.(true) | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, [props.onClose]) | ||
|
|
||
| return ( | ||
| <div className={styles.wrap}> | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <div className={styles.inner}> | ||
| {props.icon || ( | ||
| <div className={styles.icon}> | ||
| <InformationCircleIcon className='icon-xl' /> | ||
| </div> | ||
| )} | ||
|
|
||
| {props.content} | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| {!props.persistent && ( | ||
| <div className={styles.close} onClick={handleClose}> | ||
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
vas3a marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <IconOutline.XIcon className='icon-lg' /> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export default NotificationBanner | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default as NotificationBanner } from './NotificationBanner' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './banner' | ||
| export { default as Notification } from './Notification' |
Uh oh!
There was an error while loading. Please reload this page.