Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/apps/platform/src/PlatformApp.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from 'react'
import { toast, ToastContainer } from 'react-toastify'

import { useViewportUnitsFix } from '~/libs/shared'
import { NotificationsContainer, useViewportUnitsFix } from '~/libs/shared'

import { AppFooter } from './components/app-footer'
import { AppHeader } from './components/app-header'
Expand All @@ -14,6 +14,7 @@ const PlatformApp: FC<{}> = () => {
return (
<Providers>
<AppHeader />
<NotificationsContainer />
<div className='root-container'>
<PlatformRouter />
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/apps/platform/src/providers/Providers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC, ReactNode } from 'react'

import { authUrlLogout, ProfileProvider } from '~/libs/core'
import { ConfigContextProvider } from '~/libs/shared'
import { ConfigContextProvider, NotificationProvider } from '~/libs/shared'

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

Expand All @@ -13,7 +13,9 @@ const Providers: FC<ProvidersProps> = props => (
<ConfigContextProvider logoutUrl={authUrlLogout}>
<ProfileProvider>
<PlatformRouterProvider>
{props.children}
<NotificationProvider>
{props.children}
</NotificationProvider>
</PlatformRouterProvider>
</ProfileProvider>
</ConfigContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -226,6 +227,7 @@ const computePhaseCompletionFromScreenings = (

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

useEffect(() => {
const notification = showBannerNotification({
id: 'ai-review-scores-warning',
message: `AI Review Scores are advisory only to provide immediate,
educational, and actionable feedback to members.
AI Review Scores do not influence winner selection.`,
})
return () => notification && removeNotification(notification.id)
}, [showBannerNotification])

return (
<PageWrapper
pageTitle={challengeInfo?.name ?? ''}
Expand Down
2 changes: 1 addition & 1 deletion src/libs/core/lib/profile/profile-context/index.ts
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
@@ -1,4 +1,4 @@
import { Context, createContext } from 'react'
import { Context, createContext, useContext } from 'react'

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

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

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

export const useProfileContext = (): ProfileContextData => useContext(profileContext)

export default profileContext
1 change: 1 addition & 0 deletions src/libs/shared/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './modals'
export * from './profile-picture'
export * from './input-skill-selector'
export * from './member-skill-editor'
export * from './notifications'
export * from './skill-pill'
export * from './expandable-list'
export * from './grouped-skills-ui'
Expand Down
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}>
{notifications.map(n => (
<Notification key={n.id} notification={n} onClose={removeNotification} />
))}
</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'
const [notifications, setNotifications] = useState<Notification[]>([])

const removeNotification = useCallback((id: string, persist?: boolean) => {
setNotifications(prev => prev.filter(n => n.id !== id))
if (persist) {
dismiss(id)
}
}, [])

const notify = useCallback(
(message: NotifyPayload, type: NotificationType = 'info', duration = 3000) => {
const id = `${uuid}[${typeof message === 'string' ? message : message.id}]`
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)
}

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;
}
2 changes: 2 additions & 0 deletions src/libs/shared/lib/components/notifications/index.ts
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'

export const wasDismissed = (id: string): boolean => (
(localStorage.getItem(`${lsKeyPrefix}[${id}]`)) !== null
)

export const dismiss = (id: string): void => {
localStorage.setItem(`${lsKeyPrefix}[${id}]`, JSON.stringify(true))
}
1 change: 1 addition & 0 deletions src/libs/ui/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './content-layout'
export * from './default-member-icon'
// NOTE: for some reason, modals needs to be imported prior to form
export * from './modals'
export * from './notification'
export * from './form'
export * from './loading-spinner'
export * from './page-divider'
Expand Down
33 changes: 33 additions & 0 deletions src/libs/ui/lib/components/notification/Notification.tsx
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;
message: string;
type: string;
}
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}
/>
)
}

return <></>
}

export default Notification
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@import '../../../styles/includes';

.wrap {
background: #60267D;
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%;
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,
description: 'Set to true to hide the close icon button',
},
},
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'

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(() => {
props.onClose?.(true)
}, [props.onClose])

return (
<div className={styles.wrap}>
<div className={styles.inner}>
{props.icon || (
<div className={styles.icon}>
<InformationCircleIcon className='icon-xl' />
</div>
)}

{props.content}

{!props.persistent && (
<div className={styles.close} onClick={handleClose}>
<IconOutline.XIcon className='icon-lg' />
</div>
)}
</div>
</div>
)
}

export default NotificationBanner
1 change: 1 addition & 0 deletions src/libs/ui/lib/components/notification/banner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as NotificationBanner } from './NotificationBanner'
2 changes: 2 additions & 0 deletions src/libs/ui/lib/components/notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './banner'
export { default as Notification } from './Notification'