-
Notifications
You must be signed in to change notification settings - Fork 139
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
feat: JSON Notifications #420
base: develop
Are you sure you want to change the base?
Changes from 21 commits
12b7b76
7219f0c
6e233c7
72780aa
adf1fee
96bffe7
263fee7
40cc10e
ac6e8f0
294d571
85d939c
1a83e39
a21d4df
2edb63e
2a231a4
c56dcf3
ad3756c
8ca39e8
4420084
552a581
47df76f
b096afa
117c1e3
6cec903
3502a88
b4d5e6c
a025b33
dd158bb
8deea87
46a8bfb
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 |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { FC } from 'react'; | ||
import styled from 'styled-components'; | ||
|
||
interface BannerProps { | ||
children: React.ReactNode; | ||
} | ||
|
||
export const Banner: FC<BannerProps> = ({ children }) => { | ||
return <>{children}</>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './Banner'; | ||
export * from './Box'; | ||
export * from './Button'; | ||
export * from './Card'; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { FC } from 'react'; | ||
import { first } from 'lodash'; | ||
import styled from 'styled-components'; | ||
import { TransitionGroup, CSSTransition } from 'react-transition-group'; | ||
|
||
import { useAppSelector, useAppDispatch } from '@hooks'; | ||
import { NotificationsActions, NotificationsSelectors } from '@store'; | ||
import { Banner, Markdown, Icon, CloseIcon, InfoIcon, WarningFilledIcon, WarningIcon } from '@components/common'; | ||
|
||
const StyledCloseIcon = styled(Icon)` | ||
width: 1.5rem; | ||
height: 1rem; | ||
margin-top: 2.5px; | ||
cursor: pointer; | ||
&:hover { | ||
opacity: 0.7; | ||
fill: ${({ theme }) => theme.colors.primaryVariant}; | ||
} | ||
`; | ||
|
||
const StyledSymbol = styled(Icon)` | ||
width: 2.5rem; | ||
height: 1.6rem; | ||
margin-right: -2.5px; | ||
`; | ||
|
||
const StyledMarkdown = styled(Markdown)` | ||
a { | ||
text-decoration: underline; | ||
} | ||
`; | ||
|
||
const StyledNotification = styled.div` | ||
display: flex; | ||
align-items: center; | ||
> * { | ||
padding-right: 5px; | ||
} | ||
`; | ||
|
||
const StyledNotificationBanner = styled.div` | ||
background: ${({ theme }) => theme.colors.surface}; | ||
height: rem; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
position: relative; | ||
`; | ||
|
||
const StyledTransitionGroup = styled(TransitionGroup)` | ||
.transition-appear { | ||
opacity: 0; | ||
} | ||
.transition-appear-active { | ||
opacity: 1; | ||
transition: opacity 800ms; | ||
} | ||
.transition-enter { | ||
opacity: 0; | ||
} | ||
.transition-enter-active { | ||
opacity: 1; | ||
transition: opacity 800ms; | ||
} | ||
.transition-exit { | ||
opacity: 1; | ||
} | ||
.transition-exit-active { | ||
opacity: 0; | ||
position: absolute; | ||
transition: opacity 800ms; | ||
left: 0; | ||
right: 0; | ||
top: 0; | ||
bottom: 0; | ||
display: flex; | ||
justify-content: center; | ||
} | ||
`; | ||
|
||
export const NotificationBanner: FC = () => { | ||
const latestMessage = first(useAppSelector(NotificationsSelectors.selectActiveMessages)); | ||
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. is there a reason we need a container component instead of just having this all on the banner component? |
||
const dispatch = useAppDispatch(); | ||
if (!latestMessage) { | ||
return null; | ||
} | ||
|
||
let messageSymbol: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string | undefined }>; | ||
switch (latestMessage.type) { | ||
case 'info': | ||
messageSymbol = InfoIcon; | ||
break; | ||
case 'warning': | ||
messageSymbol = WarningFilledIcon; | ||
break; | ||
case 'critical': | ||
messageSymbol = WarningIcon; | ||
break; | ||
default: | ||
messageSymbol = InfoIcon; | ||
} | ||
|
||
return ( | ||
<StyledNotificationBanner> | ||
<StyledTransitionGroup> | ||
<CSSTransition appear={true} key={latestMessage.id} timeout={800} classNames={'transition'}> | ||
<Banner> | ||
<StyledNotification> | ||
<StyledSymbol Component={messageSymbol} /> | ||
<StyledMarkdown>{latestMessage.message}</StyledMarkdown> | ||
<StyledCloseIcon | ||
Component={CloseIcon} | ||
onClick={() => dispatch(NotificationsActions.dismissMessage(latestMessage.id))} | ||
/> | ||
</StyledNotification> | ||
</Banner> | ||
</CSSTransition> | ||
</StyledTransitionGroup> | ||
</StyledNotificationBanner> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[] | ||
xgambitox marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { createAction, createAsyncThunk } from '@reduxjs/toolkit'; | ||
|
||
import { ThunkAPI } from '@frameworks/redux'; | ||
import { Message } from '@types'; | ||
import notificationMessages from '@client/data/notificationMessages.json'; | ||
|
||
const dismissMessage = createAction<number>('notifications/dismissMessage'); | ||
|
||
/* -------------------------------------------------------------------------- */ | ||
/* Fetch Data */ | ||
/* -------------------------------------------------------------------------- */ | ||
const importedNotifications = notificationMessages as Message[]; | ||
|
||
const getNotificationMessages = createAsyncThunk<Message[], void, ThunkAPI>( | ||
'notifications/getNotificationMessages', | ||
async () => { | ||
return importedNotifications; | ||
xgambitox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
); | ||
|
||
/* -------------------------------------------------------------------------- */ | ||
/* Exports */ | ||
/* -------------------------------------------------------------------------- */ | ||
|
||
export const NotificationsActions = { | ||
getNotificationMessages, | ||
dismissMessage, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { createReducer } from '@reduxjs/toolkit'; | ||
|
||
import { initialStatus, NotificationsState } from '@types'; | ||
|
||
import { NotificationsActions } from './notifications.actions'; | ||
|
||
export const notificationsInitialState: NotificationsState = { | ||
messages: [], | ||
dismissedMessageIds: [], | ||
statusMap: { | ||
getNotificationMessages: { ...initialStatus }, | ||
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. you can just do |
||
}, | ||
}; | ||
|
||
const { getNotificationMessages, dismissMessage } = NotificationsActions; | ||
|
||
const notificationsReducer = createReducer(notificationsInitialState, (builder) => { | ||
builder | ||
.addCase(dismissMessage, (state, actions) => { | ||
state.dismissedMessageIds = [...state.dismissedMessageIds, actions.payload]; | ||
}) | ||
|
||
/* -------------------------------------------------------------------------- */ | ||
/* Fetch Data */ | ||
/* -------------------------------------------------------------------------- */ | ||
.addCase(getNotificationMessages.pending, (state) => { | ||
state.statusMap.getNotificationMessages = { loading: true }; | ||
}) | ||
|
||
.addCase(getNotificationMessages.rejected, (state, { error }) => { | ||
state.statusMap.getNotificationMessages = { loading: false, error: error.message }; | ||
}) | ||
|
||
.addCase(getNotificationMessages.fulfilled, (state, { payload: messages }) => { | ||
state.messages = messages.filter((message) => { | ||
return message.active === true; | ||
}); | ||
state.statusMap.getNotificationMessages = {}; | ||
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. this should be 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. it does make sense to set it as initialStatus, since after the request is fulfilled, loading should be false and there shouldn't be an error. I noticed in other reducers however that the .fulfilled is set to {} 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. I prefer being more explicit and having |
||
}); | ||
}); | ||
|
||
export default notificationsReducer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { createSelector } from '@reduxjs/toolkit'; | ||
|
||
import { RootState, Message } from '@types'; | ||
|
||
/* ---------------------------------- State --------------------------------- */ | ||
const selectDismissedMessageIds = (state: RootState) => state.notifications.dismissedMessageIds; | ||
const selectMessages = (state: RootState) => state.notifications.messages; | ||
const selectActiveMessages = createSelector<RootState, Message[], Number[], Message[]>( | ||
[selectMessages, selectDismissedMessageIds], | ||
(messages, dismissedMessageIds) => { | ||
const activeMessages = messages.filter((message) => { | ||
TacosTonight marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return !dismissedMessageIds.includes(message.id); | ||
}); | ||
return activeMessages; | ||
} | ||
); | ||
|
||
/* --------------------------------- Exports -------------------------------- */ | ||
export const NotificationsSelectors = { | ||
selectDismissedMessageIds, | ||
selectMessages, | ||
selectActiveMessages, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export type Classification = 'info' | 'warning' | 'critical'; | ||
|
||
export interface Message { | ||
id: number; | ||
type: Classification; | ||
active: boolean; | ||
message: string; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can probably remove this file since it does nothing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This component should be the presentational component. I suggest moving all the styling from
NotificationBanner
to this one.Banner
should receive props like children and onClose.