Skip to content

Commit

Permalink
add platform pusher notification service
Browse files Browse the repository at this point in the history
  • Loading branch information
shivamkantival committed Mar 28, 2020
1 parent e97d5b6 commit cd48874
Show file tree
Hide file tree
Showing 22 changed files with 427 additions and 20 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-transition-group": "^4.2.4",
"@types/styled-components": "^5.0.1",
"lodash": "^4.17.15",
"query-string": "^6.11.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"react-transition-group": "^4.3.0",
"reselect": "^4.0.0",
"styled-components": "^5.0.1",
"typescript": "~3.7.2"
},
Expand Down
12 changes: 7 additions & 5 deletions src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import React from 'react';
import Header from 'components/Header';
import StyledAppContainer from './styles';
import AppContent from 'components/AppContent';
import PlatformPusherNotifications from 'containers/PlatformPusherNotifications';

function App() {
return (
<StyledAppContainer>
<Header className="header" />
<AppContent className="appContent" />
</StyledAppContainer>
<PlatformPusherNotifications>
<StyledAppContainer>
<Header className="header" />
<AppContent className="appContent" />
</StyledAppContainer>
</PlatformPusherNotifications>
);
}

export default App;
File renamed without changes.
2 changes: 2 additions & 0 deletions src/config/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './actions';
export * from './notifications';
11 changes: 11 additions & 0 deletions src/config/interfaces/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum NOTIFICATION_TYPES {
SUCCESS,
ERROR,
INFO,
}

export type NewNotificationDetails = {
readonly timeout?: number;
readonly type: NOTIFICATION_TYPES;
readonly message: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { memo } from 'react';

//typeDefs
import { Notification } from '../../interfaces';

//components
import StyledNotificationCard from './styles';

type NotificationCardProps = {
notification: Notification;
};

const NotificationCard: React.FC<NotificationCardProps> = ({ notification }) => (
<StyledNotificationCard type={notification.type}>{notification.message}</StyledNotificationCard>
);

export default memo<NotificationCardProps>(NotificationCard);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styled from 'styled-components';
import { COLORS, spacing } from 'styles/theme';
import { NOTIFICATION_TYPES } from 'config/interfaces';
import { flexContainer } from 'styles/mixins';

const NOTIFICATION_TYPE_TO_BACKGROUND_COLOR = {
[NOTIFICATION_TYPES.SUCCESS]: COLORS.PASTEL_GREEN,
[NOTIFICATION_TYPES.ERROR]: COLORS.BITTERSWEET,
[NOTIFICATION_TYPES.INFO]: COLORS.HONEY,
};

type StyledNotificationCardProps = {
type: NOTIFICATION_TYPES;
};

export default styled.span`
height: 100%;
margin-bottom: ${spacing(2)};
border-radius: ${spacing(1)};
padding: ${spacing(2)};
background: ${(props: StyledNotificationCardProps) =>
NOTIFICATION_TYPE_TO_BACKGROUND_COLOR[props.type]};
${flexContainer({ alignItems: 'center', justifyContent: 'flex-end' })}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { memo, ReactNode } from 'react';

//typeDefs
import { Notification } from '../../interfaces';

//components
import StyledNotificationsView from './style';
import NotificationCard from '../NotificationCard';
import { CSSTransition, TransitionGroup } from 'react-transition-group';

//utils
import map from 'lodash/map';

type NotificationsProps = {
notifications: ReadonlyArray<Notification>;
};

const Notifications: React.FC<NotificationsProps> = ({ notifications }) => {
return (
<StyledNotificationsView>
<TransitionGroup enter exit className="notificationListContainer">
{map<Notification, ReactNode>(
notifications,
(notif: Notification): ReactNode => (
<CSSTransition classNames="animation" timeout={500} key={notif.id} in>
<NotificationCard notification={notif} />
</CSSTransition>
)
)}
</TransitionGroup>
</StyledNotificationsView>
);
};

export default memo<NotificationsProps>(Notifications);
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import styled from 'styled-components';
import { spacing } from 'styles/theme';
import { flexContainer } from 'styles/mixins';

const CARD_HEIGHT = spacing(10);

export default styled.div`
.notificationListContainer {
${flexContainer({ flexDirection: 'column', alignItems: 'flex-end' })}
}
.animation-enter-done {
height: ${CARD_HEIGHT};
}
.animation-enter {
opacity: 1;
height: ${CARD_HEIGHT};
}
.animation-enter-active {
animation: enter 500ms ease-out;
position: relative;
@keyframes enter {
from {
opacity: 0;
height: 0;
right: -${spacing(5)};
}
75% {
right: ${spacing(4)};
}
to {
opacity: 1;
height: ${CARD_HEIGHT};
right: 0;
}
}
}
.animation-exit-done {
height: 0;
}
.animation-exit {
opacity: 0;
height: ${CARD_HEIGHT};
}
.animation-exit-active {
animation: exit 500ms ease-out;
position: relative;
@keyframes exit {
from {
opacity: 1;
height: ${CARD_HEIGHT};
right: 0;
}
25% {
right: ${spacing(4)};
}
to {
opacity: 0;
height: 0;
right: -${spacing(8)};
}
}
}
position: fixed;
bottom: ${spacing(10)};
right: ${spacing(5)};
`;
18 changes: 18 additions & 0 deletions src/containers/PlatformPusherNotifications/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//typeDefs
import { NewNotificationDetails, Notification } from './interfaces';

//utils
import uniqueId from 'lodash/uniqueId';
import pick from 'lodash/pick';
import { PausableTimer, OnTimeout } from 'utils/pausableTimer';

export function createNewNotificationFromNotifDetails(
notifDetails: NewNotificationDetails,
{ onTimeout }: { onTimeout: OnTimeout }
): Notification {
return {
...pick<typeof notifDetails, 'message' | 'type'>(notifDetails, ['message', 'type']),
id: uniqueId('notification_object_'),
timer: new PausableTimer(onTimeout, notifDetails.timeout || 5000),
};
}
73 changes: 73 additions & 0 deletions src/containers/PlatformPusherNotifications/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { memo, useCallback, useMemo, useState } from 'react';

//typedefs
import { Notification, PlatformPusherProps } from './interfaces';
import { OnTimeout } from 'utils';

//constants
import { EMPTY_ARRAY_READONLY } from 'config/constants';

//utils
import concat from 'lodash/concat';
import remove from 'lodash/remove';
import slice from 'lodash/slice';
import { createNewNotificationFromNotifDetails } from './helpers';

//context
import PlatformPusherNotification, {
PlatformPusherNotificationContext,
PushNotification,
} from 'context/PlatformPusherNotification';

//components
import NotificationsView from './components/Notifications';

const PlatformPusherNotifications: React.FC<PlatformPusherProps> = props => {
const [notifications, setNotifications] = useState<ReadonlyArray<Notification>>(
EMPTY_ARRAY_READONLY
);

const handleNotifTimeout: OnTimeout = useCallback<OnTimeout>(timer => {
setNotifications(currNotifications => {
const shallowCopiedNotifications = [...currNotifications];
remove<Notification>(
shallowCopiedNotifications,
(notif: Notification): boolean => notif.timer === timer
);
return shallowCopiedNotifications;
});
}, []);

const pushNotification = useCallback<PushNotification>(
notifDetails => {
setNotifications(currNotif =>
concat<Notification>(
currNotif,
createNewNotificationFromNotifDetails(notifDetails, { onTimeout: handleNotifTimeout })
)
);
},
[handleNotifTimeout]
);

const context = useMemo<PlatformPusherNotificationContext>(
() => ({
pushNotification: pushNotification,
}),
[pushNotification]
);

const oldestFiveNotifs = useMemo<typeof notifications>(
() => slice<Notification>(notifications, 0, 5),
[notifications]
);

return (
<PlatformPusherNotification.Provider value={context}>
<NotificationsView notifications={oldestFiveNotifs} />
{props.children}
</PlatformPusherNotification.Provider>
);
};

export default memo<PlatformPusherProps>(PlatformPusherNotifications);
14 changes: 14 additions & 0 deletions src/containers/PlatformPusherNotifications/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { PausableTimer } from 'utils';
import { NOTIFICATION_TYPES } from 'config/interfaces';

export * from 'config/interfaces/notifications';

export type Notification = {
readonly id: string;
readonly type: NOTIFICATION_TYPES;
readonly message: string;
readonly timer: PausableTimer;
};

export type PlatformPusherProps = { children: React.ReactNode };
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const UsersListView = forwardRef<HTMLDivElement, UsersListViewProps>((props, ref
className={isLoadingForFirstTime ? 'fullPageLoad' : ''}
>
{isLoadingForFirstTime ? (
<Loader dimension={40} className="loader" />
<Loader dimension={25} className="loader" />
) : (
<>
{map<User, ReactElement>(users, user => (
Expand Down
15 changes: 15 additions & 0 deletions src/containers/UsersList/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function getMessageForNotificationPostDataFetch({
hasError,
total,
currentDataLength,
}: {
readonly hasError: boolean;
readonly total: number;
readonly currentDataLength: number;
}): string {
if (hasError) {
return 'Couldn\'t fetch users';
} else {
return total === currentDataLength ? 'Fetched all users!' : 'Users fetched, more to go!';
}
}
Loading

0 comments on commit cd48874

Please sign in to comment.