From 20047af21b3835b311c83123222f0104821e4195 Mon Sep 17 00:00:00 2001 From: amy-corson-ibigroup <115499534+amy-corson-ibigroup@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:06:31 -0600 Subject: [PATCH] feat: add network connection status to ui and redux --- i18n/en-US.yml | 2 + lib/actions/ui.js | 3 + lib/components/app/desktop-nav.tsx | 8 ++ .../app/network-connection-banner.tsx | 78 +++++++++++++++++++ lib/components/app/responsive-webapp.js | 6 +- lib/components/mobile/navigation-bar.js | 9 ++- lib/reducers/create-otp-reducer.js | 12 +++ 7 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 lib/components/app/network-connection-banner.tsx diff --git a/i18n/en-US.yml b/i18n/en-US.yml index e3f9c8025..9bb360c76 100644 --- a/i18n/en-US.yml +++ b/i18n/en-US.yml @@ -183,6 +183,8 @@ components: closeMenu: Close Menu fieldTrip: Field Trip mailables: Mailables + networkConnectionLost: Network connection lost + networkConnectionRestored: Network connection restored openMenu: Open Menu skipNavigation: Skip navigation BackToTripPlanner: diff --git a/lib/actions/ui.js b/lib/actions/ui.js index cdd0691da..a548a43bc 100644 --- a/lib/actions/ui.js +++ b/lib/actions/ui.js @@ -47,6 +47,9 @@ const viewRoute = createAction('SET_VIEWED_ROUTE') export const toggleAutoRefresh = createAction('TOGGLE_AUTO_REFRESH') const settingItineraryView = createAction('SET_ITINERARY_VIEW') export const setPopupContent = createAction('SET_POPUP_CONTENT') +export const setNetworkConnectionLost = createAction( + 'SET_NETWORK_CONNECTION_LOST' +) // This code-less action calls the reducer code // and thus resets the session timeout. diff --git a/lib/components/app/desktop-nav.tsx b/lib/components/app/desktop-nav.tsx index f9a265b86..ef692d88c 100644 --- a/lib/components/app/desktop-nav.tsx +++ b/lib/components/app/desktop-nav.tsx @@ -13,6 +13,7 @@ import { DEFAULT_APP_TITLE } from '../../util/constants' import InvisibleA11yLabel from '../util/invisible-a11y-label' import NavLoginButtonAuth0 from '../user/nav-login-button-auth0' +import { NetworkConnectionBanner } from './network-connection-banner' import AppMenu, { Icon } from './app-menu' import LocaleSelector from './locale-selector' import NavbarItem from './nav-item' @@ -63,6 +64,7 @@ const NavItemOnLargeScreens = styled(NavbarItem)` // Typscript TODO: otpConfig type export type Props = { locale: string + networkConnectionLost: boolean otpConfig: AppConfig popupTarget?: string setPopupContent: (url: string) => void @@ -83,6 +85,7 @@ export type Props = { */ const DesktopNav = ({ locale, + networkConnectionLost, otpConfig, popupTarget, setPopupContent @@ -117,6 +120,8 @@ const DesktopNav = ({ id: `config.popups.${popupTarget}` }) + console.log(networkConnectionLost) + return (
@@ -166,6 +171,7 @@ const DesktopNav = ({ +
) } @@ -173,8 +179,10 @@ const DesktopNav = ({ // connect to the redux store const mapStateToProps = (state: AppReduxState) => { const { config: otpConfig } = state.otp + const { networkConnectionLost } = state.otp.ui.errors return { locale: state.otp.ui.locale, + networkConnectionLost, otpConfig, popupTarget: otpConfig.popups?.launchers?.toolbar } diff --git a/lib/components/app/network-connection-banner.tsx b/lib/components/app/network-connection-banner.tsx new file mode 100644 index 000000000..9ea7de51b --- /dev/null +++ b/lib/components/app/network-connection-banner.tsx @@ -0,0 +1,78 @@ +import { CSSTransition, TransitionGroup } from 'react-transition-group' +import { FormattedMessage } from 'react-intl' +import React, { useRef } from 'react' +import styled from 'styled-components' + +import { RED_ON_WHITE } from '../util/colors' + +const containerClassname = 'network-connection-banner' +const timeout = 200 + +const TransitionStyles = styled.div` + .${containerClassname} { + background: ${RED_ON_WHITE}; + border-left: 1px solid #e7e7e7; + border-right: 1px solid #e7e7e7; + color: #fff; + font-weight: 600; + padding: 5px; + position: absolute; + text-align: center; + top: 50px; + width: 100%; + z-index: 1; + } + .${containerClassname}-enter { + opacity: 0; + transform: translateY(-100%); + } + .${containerClassname}-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity ${timeout}ms ease-in; + } + .${containerClassname}-exit { + opacity: 1; + transform: translateY(0); + } + .${containerClassname}-exit-active { + opacity: 0; + transform: translateY(-100%); + transition: opacity ${timeout}ms ease-in, transform ${timeout}ms ease-in; + } +` +export const NetworkConnectionLostBanner = styled.div`` + +export const NetworkConnectionBanner = ({ + networkConnectionLost +}: { + networkConnectionLost: boolean +}): JSX.Element => { + const connectionLostBannerRef = useRef(null) + return ( + + + {networkConnectionLost && ( + + + {networkConnectionLost ? ( + + ) : ( + + )} + + + )} + + + ) +} diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 2b2a44dfc..15e5d4a22 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -154,10 +154,13 @@ class ResponsiveWebapp extends Component { map, matchContentToUrl, parseUrlQueryString, - receivedPositionResponse + receivedPositionResponse, + setNetworkConnectionLost } = this.props // Add on back button press behavior. window.addEventListener('popstate', handleBackButtonPress) + window.addEventListener('online', () => setNetworkConnectionLost(false)) + window.addEventListener('offline', () => setNetworkConnectionLost(true)) // If a URL is detected without hash routing (e.g., http://localhost:9966?sessionId=test), // window.location.search will have a value. In this case, we need to redirect to the URL root with the @@ -441,6 +444,7 @@ const mapStateToWrapperProps = (state) => { const mapWrapperDispatchToProps = { processSignIn: authActions.processSignIn, setLocale: uiActions.setLocale, + setNetworkConnectionLost: uiActions.setNetworkConnectionLost, showAccessTokenError: authActions.showAccessTokenError, showLoginError: authActions.showLoginError } diff --git a/lib/components/mobile/navigation-bar.js b/lib/components/mobile/navigation-bar.js index 4426a244c..eaa071f3d 100644 --- a/lib/components/mobile/navigation-bar.js +++ b/lib/components/mobile/navigation-bar.js @@ -9,6 +9,7 @@ import * as uiActions from '../../actions/ui' import { accountLinks, getAuth0Config } from '../../util/auth' import { ComponentContext } from '../../util/contexts' import { injectIntl } from 'react-intl' +import { NetworkConnectionBanner } from '../app/network-connection-banner' import { StyledIconWrapper } from '../util/styledIcon' import AppMenu from '../app/app-menu' import LocaleSelector from '../app/locale-selector' @@ -32,6 +33,7 @@ class MobileNavigationBar extends Component { headerText: PropTypes.string, intl: PropTypes.object, locale: PropTypes.string, + networkConnectionLost: PropTypes.bool, onBackClicked: PropTypes.func, setMobileScreen: PropTypes.func, showBackButton: PropTypes.bool @@ -55,6 +57,7 @@ class MobileNavigationBar extends Component { headerText, intl, locale, + networkConnectionLost, showBackButton } = this.props @@ -111,6 +114,9 @@ class MobileNavigationBar extends Component { )} + ) } @@ -123,7 +129,8 @@ const mapStateToProps = (state) => { auth0Config: getAuth0Config(state.otp.config.persistence), configLanguages: state.otp.config.language, extraMenuItems: state.otp?.config?.extraMenuItems, - locale: state.otp.ui.locale + locale: state.otp.ui.locale, + networkConnectionLost: state.otp.ui.errors.networkConnectionLost } } diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js index e753c466c..0028f5095 100644 --- a/lib/reducers/create-otp-reducer.js +++ b/lib/reducers/create-otp-reducer.js @@ -225,6 +225,9 @@ export function getInitialState(userDefinedConfig) { }, ui: { diagramLeg: null, + errors: { + networkConnectionLost: !navigator.onLine + }, locale: null, localizedMessages: null, mainPanelContent: null, @@ -1108,6 +1111,15 @@ function createOtpReducer(config) { } } }) + case 'SET_NETWORK_CONNECTION_LOST': + console.log(action.payload) + return update(state, { + ui: { + errors: { + networkConnectionLost: { $set: action.payload } + } + } + }) case 'SERVICE_TIME_RANGE_REQUEST': return update(state, { serviceTimeRange: { $set: { pending: true } }