Skip to content

Commit 52a803c

Browse files
authored
Merge pull request #1265 from topcoder-platform/PM-2133_dismissable-banner
PM-2133 dismissable banner
2 parents d6ad95d + ca1e432 commit 52a803c

File tree

18 files changed

+299
-5
lines changed

18 files changed

+299
-5
lines changed

src/apps/platform/src/PlatformApp.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC } from 'react'
22
import { toast, ToastContainer } from 'react-toastify'
33

4-
import { useViewportUnitsFix } from '~/libs/shared'
4+
import { NotificationsContainer, useViewportUnitsFix } from '~/libs/shared'
55

66
import { AppFooter } from './components/app-footer'
77
import { AppHeader } from './components/app-header'
@@ -14,6 +14,7 @@ const PlatformApp: FC<{}> = () => {
1414
return (
1515
<Providers>
1616
<AppHeader />
17+
<NotificationsContainer />
1718
<div className='root-container'>
1819
<PlatformRouter />
1920
</div>

src/apps/platform/src/providers/Providers.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC, ReactNode } from 'react'
22

33
import { authUrlLogout, ProfileProvider } from '~/libs/core'
4-
import { ConfigContextProvider } from '~/libs/shared'
4+
import { ConfigContextProvider, NotificationProvider } from '~/libs/shared'
55

66
import { PlatformRouterProvider } from './platform-router.provider'
77

@@ -13,7 +13,9 @@ const Providers: FC<ProvidersProps> = props => (
1313
<ConfigContextProvider logoutUrl={authUrlLogout}>
1414
<ProfileProvider>
1515
<PlatformRouterProvider>
16-
{props.children}
16+
<NotificationProvider>
17+
{props.children}
18+
</NotificationProvider>
1719
</PlatformRouterProvider>
1820
</ProfileProvider>
1921
</ConfigContextProvider>

src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { TableLoading } from '~/apps/admin/src/lib'
1313
import { handleError } from '~/apps/admin/src/lib/utils'
1414
import { EnvironmentConfig } from '~/config'
1515
import { BaseModal, Button, InputCheckbox, InputText } from '~/libs/ui'
16+
import { NotificationContextType, useNotification } from '~/libs/shared'
1617

1718
import {
1819
useFetchScreeningReview,
@@ -226,6 +227,7 @@ const computePhaseCompletionFromScreenings = (
226227

227228
// eslint-disable-next-line complexity
228229
export const ChallengeDetailsPage: FC<Props> = (props: Props) => {
230+
const { showBannerNotification, removeNotification }: NotificationContextType = useNotification()
229231
const [searchParams, setSearchParams] = useSearchParams()
230232
const location = useLocation()
231233
const navigate = useNavigate()
@@ -1323,6 +1325,16 @@ export const ChallengeDetailsPage: FC<Props> = (props: Props) => {
13231325
: undefined
13241326
const shouldShowChallengeMetaRow = Boolean(statusLabel) || trackTypePills.length > 0
13251327

1328+
useEffect(() => {
1329+
const notification = showBannerNotification({
1330+
id: 'ai-review-scores-warning',
1331+
message: `AI Review Scores are advisory only to provide immediate,
1332+
educational, and actionable feedback to members.
1333+
AI Review Scores do not influence winner selection.`,
1334+
})
1335+
return () => notification && removeNotification(notification.id)
1336+
}, [showBannerNotification])
1337+
13261338
return (
13271339
<PageWrapper
13281340
pageTitle={challengeInfo?.name ?? ''}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './profile-context-data.model'
2-
export { default as profileContext, defaultProfileContextData } from './profile.context'
2+
export { default as profileContext, defaultProfileContextData, useProfileContext } from './profile.context'
33
export * from './profile.context-provider'

src/libs/core/lib/profile/profile-context/profile.context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Context, createContext } from 'react'
1+
import { Context, createContext, useContext } from 'react'
22

33
import { ProfileContextData } from './profile-context-data.model'
44

@@ -12,4 +12,6 @@ export const defaultProfileContextData: ProfileContextData = {
1212

1313
const profileContext: Context<ProfileContextData> = createContext(defaultProfileContextData)
1414

15+
export const useProfileContext = (): ProfileContextData => useContext(profileContext)
16+
1517
export default profileContext

src/libs/shared/lib/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './modals'
33
export * from './profile-picture'
44
export * from './input-skill-selector'
55
export * from './member-skill-editor'
6+
export * from './notifications'
67
export * from './skill-pill'
78
export * from './expandable-list'
89
export * from './grouped-skills-ui'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { FC } from 'react'
2+
3+
import { Notification } from '~/libs/ui'
4+
5+
import { NotificationContextType, useNotification } from './Notifications.context'
6+
import styles from './NotificationsContainer.module.scss'
7+
8+
const NotificationsContainer: FC = () => {
9+
const { notifications, removeNotification }: NotificationContextType = useNotification()
10+
11+
return (
12+
<div className={styles.wrap}>
13+
{notifications.map(n => (
14+
<Notification key={n.id} notification={n} onClose={removeNotification} />
15+
))}
16+
</div>
17+
)
18+
}
19+
20+
export default NotificationsContainer
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react'
2+
3+
import { useProfileContext } from '~/libs/core'
4+
5+
import { dismiss, wasDismissed } from './localstorage.utils'
6+
7+
export type NotificationType = 'success' | 'error' | 'info' | 'warning' | 'banner';
8+
9+
export interface Notification {
10+
id: string;
11+
type: NotificationType;
12+
icon?: ReactNode
13+
message: string;
14+
duration?: number; // in ms
15+
}
16+
17+
type NotifyPayload = string | (Partial<Notification> & { message: string })
18+
19+
export interface NotificationContextType {
20+
notifications: Notification[];
21+
notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void;
22+
showBannerNotification: (message: NotifyPayload) => Notification | void;
23+
removeNotification: (id: string) => void;
24+
}
25+
26+
const NotificationContext = createContext<NotificationContextType | undefined>(undefined)
27+
28+
export const useNotification = (): NotificationContextType => {
29+
const context = useContext(NotificationContext)
30+
if (!context) throw new Error('useNotification must be used within a NotificationProvider')
31+
return context
32+
}
33+
34+
export const NotificationProvider: React.FC<{
35+
children: ReactNode,
36+
}> = props => {
37+
const profileCtx = useProfileContext()
38+
const uuid = profileCtx.profile?.userId ?? 'annon'
39+
const [notifications, setNotifications] = useState<Notification[]>([])
40+
41+
const removeNotification = useCallback((id: string, persist?: boolean) => {
42+
setNotifications(prev => prev.filter(n => n.id !== id))
43+
if (persist) {
44+
dismiss(id)
45+
}
46+
}, [])
47+
48+
const notify = useCallback(
49+
(message: NotifyPayload, type: NotificationType = 'info', duration = 3000) => {
50+
const id = `${uuid}[${typeof message === 'string' ? message : message.id}]`
51+
const newNotification: Notification
52+
= typeof message === 'string'
53+
? { duration, id, message, type }
54+
: { duration, type, ...message, id }
55+
56+
if (wasDismissed(id)) {
57+
return undefined
58+
}
59+
60+
setNotifications(prev => [...prev, newNotification])
61+
62+
if (duration > 0) {
63+
setTimeout(() => removeNotification(id), duration)
64+
}
65+
66+
return newNotification
67+
},
68+
[uuid],
69+
)
70+
71+
const showBannerNotification = useCallback((
72+
message: NotifyPayload,
73+
) => notify(message, 'banner', 0), [notify])
74+
75+
const ctxValue = useMemo(() => ({
76+
notifications,
77+
notify,
78+
removeNotification,
79+
showBannerNotification,
80+
}), [
81+
notifications,
82+
notify,
83+
removeNotification,
84+
showBannerNotification,
85+
])
86+
87+
return (
88+
<NotificationContext.Provider value={ctxValue}>
89+
{props.children}
90+
</NotificationContext.Provider>
91+
)
92+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@import "@libs/ui/styles/includes";
2+
3+
.wrap {
4+
position: relative;
5+
width: 100%;
6+
z-index: 1000;
7+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as NotificationsContainer } from './Notifications.container'
2+
export * from './Notifications.context'

0 commit comments

Comments
 (0)