diff --git a/.gitignore b/.gitignore index b122318..43e9cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +analyse.html \ No newline at end of file diff --git a/analyse.html b/analyse.html deleted file mode 100644 index b8143eb..0000000 --- a/analyse.html +++ /dev/null @@ -1,4838 +0,0 @@ - - - - - - - - Rollup Visualizer - - - -
- - - - - diff --git a/example/src/App.tsx b/example/src/App.tsx index 039afba..45b11ba 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,19 +1,15 @@ -import { - OnlineStatusNotification, - Position, - useOnlineStatus -} from '../../src/nsn' -import './App.css' -import { AppBarContainer } from './components/AppBar' -import { InfoModal } from './components/InfoModal' -import { DarkModeContainer } from './components/darkMode' -import { PositionContainer } from './components/position' import Alert from '@mui/material/Alert' import Container from '@mui/material/Container' import Divider from '@mui/material/Divider' import Grid from '@mui/material/Grid' import Stack from '@mui/material/Stack' import { useReducer, useState } from 'react' +import { OnlineStatusNotification, useOnlineStatus } from '../../src/nsn' +import './App.css' +import { AppBarContainer } from './components/AppBar' +import { InfoModal } from './components/InfoModal' +import { DarkModeContainer } from './components/darkMode' +import { PositionContainer } from './components/position' type ReducerActions = { type: @@ -23,7 +19,7 @@ type ReducerActions = { | 'offlineStatusText' | 'duration' payload: { - position?: Position + position?: any darkMode?: boolean statusText?: { online?: string; offline?: string } duration?: number @@ -33,7 +29,7 @@ type ReducerActions = { export type State = { darkMode: boolean duration: number - position: Position + position: any statusText: { online?: string offline?: string diff --git a/package-lock.json b/package-lock.json index 73dc971..7152464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-nsn", - "version": "1.2.4", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-nsn", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@types/jest": "^27.5.2", diff --git a/package.json b/package.json index cba3596..99ec745 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-nsn", - "version": "1.2.4", + "version": "1.3.0", "private": false, "description": "A very lightweight and customizable online network status notification built for react apps.", "license": "MIT", diff --git a/src/OnlineStatusNotification.tsx b/src/OnlineStatusNotification.tsx index d34207c..f62cabf 100644 --- a/src/OnlineStatusNotification.tsx +++ b/src/OnlineStatusNotification.tsx @@ -1,15 +1,15 @@ +import React, { forwardRef, useEffect } from 'react' +import { CSSTransition, SwitchTransition } from 'react-transition-group' import './App.css' import { useFirstRender } from './hooks' import { closeIcon, offlineIcon, onlineIcon } from './icons' -import React, { forwardRef, useEffect } from 'react' -import { CSSTransition, SwitchTransition } from 'react-transition-group' type StatusText = { online?: string offline?: string } -export type Position = +type Position = | 'topLeft' | 'topRight' | 'topCenter' @@ -22,7 +22,7 @@ type EventsCallback = { onCloseClick: () => void } -interface OnlineStatusNotificationType { +interface OnlineStatusNotificationProps { darkMode?: boolean destoryOnClose?: boolean duration?: number @@ -59,7 +59,7 @@ const DefaultOfflineText = 'You are currently offline.' */ const OnlineStatusNotificationComponent = forwardRef< HTMLDivElement, - OnlineStatusNotificationType + OnlineStatusNotificationProps >((props, ref): JSX.Element => { const [isOpen, setIsOpen] = React.useState(false) @@ -128,63 +128,61 @@ const OnlineStatusNotificationComponent = forwardRef< if (isFirstRender && isOnline) return null return ( - <> - - - void) => { - nodeRef.current?.addEventListener('transitionend', done, false) + + + void) => { + nodeRef.current?.addEventListener('transitionend', done, false) + }} + classNames='fade' + > +
{ + setHovering(true) + }} + onMouseLeave={() => { + setHovering(false) }} - classNames='fade' > +
+ {isOnline ? onlineIcon : offlineIcon} +
+
{getStatusText(isOnline, statusText)}
+ {/* refresh link */} + {!isOnline && ( +
+ Refresh +
+ )} + {/* close icon */}
{ - setHovering(true) - }} - onMouseLeave={() => { - setHovering(false) - }} + onClick={handleCloseButtonClick} > -
- {isOnline ? onlineIcon : offlineIcon} -
-
{getStatusText(isOnline, statusText)}
- {/* refresh link */} - {!isOnline && ( -
- Refresh -
- )} - {/* close icon */} -
- {closeIcon} -
+ {closeIcon}
- - - - +
+
+
+
) }) @@ -197,4 +195,4 @@ const getStatusText = (isOnline: boolean, statusText: StatusText): string => ? statusText?.online ?? DefaultOnlineText : statusText?.offline ?? DefaultOfflineText -const classNames = (...classes: unknown[]) => classes.filter(Boolean).join(' ') +const classNames = (...classes: string[]) => classes.filter(Boolean).join(' ') diff --git a/src/hooks.tsx b/src/hooks.tsx index 3170edf..b09e253 100644 --- a/src/hooks.tsx +++ b/src/hooks.tsx @@ -1,12 +1,12 @@ -import { NetworkInformation } from './network-information-api' -import { timeSince } from './utils' import { useCallback, useEffect, useLayoutEffect, + useReducer, useRef, useState } from 'react' +import { DEFAULT_POLLING_URL, timeSince } from './utils' const isWindowDocumentAvailable = typeof window !== 'undefined' @@ -24,11 +24,85 @@ function getConnectionInfo() { const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect -type UseOnlineStatusProps = { +type OnlineStatusProps = { pollingUrl?: string pollingDuration?: number } +const InitialOnlineStatus = + isNavigatorObjectAvailable && + isWindowDocumentAvailable && + typeof navigator?.onLine === 'boolean' + ? navigator?.onLine + : true + +type ReducerActionTypes = 'offline' | 'online' +type ReducerActions = { + type: ReducerActionTypes +} + +type State = { + online: boolean + time: { + since: Date + diff: string + } +} + +function statusReducer(prevState: State, action: ReducerActions) { + let newState: State + switch (action.type) { + case 'offline': { + if (!prevState.online) + newState = { + ...prevState, + online: false, + time: { + since: prevState.time.since, + diff: timeSince(prevState.time.since) + } + } + // previous state is online, recalculate time + else if (prevState.online) + newState = { + ...prevState, + online: false, + time: { + since: new Date(), + diff: timeSince(new Date()) + } + } + } + break + case 'online': { + if (prevState.online) + newState = { + ...prevState, + online: true, + time: { + since: prevState.time.since, + diff: timeSince(prevState.time.since) + } + } + // previous state is offline, recalculate time + else if (!prevState.online) { + newState = { + ...prevState, + online: true, + time: { + since: new Date(), + diff: timeSince(new Date()) + } + } + } + } + break + default: + newState = { ...prevState } + } + return newState +} + /** * Return current network status, status time info, network information * @example @@ -46,26 +120,18 @@ type UseOnlineStatusProps = { */ function useOnlineStatus({ - pollingUrl = 'https://www.gstatic.com/generate_204', + pollingUrl = DEFAULT_POLLING_URL, pollingDuration = 12000 -}: UseOnlineStatusProps = {}): { +}: OnlineStatusProps = {}): { attributes: { isOnline: boolean } connectionInfo: NetworkInformation - error: unknown + error: Error isOffline: boolean isOnline: boolean time: { since: Date; difference: string } } { - const [isOnline, setIsOnline] = useState<{ - online: boolean - time: { since: Date; diff: string } - }>({ - online: - isNavigatorObjectAvailable && - isWindowDocumentAvailable && - typeof navigator.onLine === 'boolean' - ? navigator.onLine - : true, + const [statusState, dispatch] = useReducer(statusReducer, { + online: InitialOnlineStatus, time: { since: new Date(), diff: timeSince(new Date()) @@ -79,42 +145,13 @@ function useOnlineStatus({ .then( (response) => response && - setIsOnline((prevState) => { - if (prevState.online) { - return { - online: true, - time: { - since: prevState.time.since, - diff: timeSince(prevState.time.since) - } - } - } - return { - online: true, - time: { - since: new Date(), - diff: timeSince(new Date()) - } - } + dispatch({ + type: 'online' }) ) .catch(() => { - return setIsOnline((prevState) => { - if (!prevState.online) - return { - online: false, - time: { - since: prevState.time.since, - diff: timeSince(prevState.time.since) - } - } - return { - online: false, - time: { - since: new Date(), - diff: timeSince(new Date()) - } - } + return dispatch({ + type: 'offline' }) }) }, [pollingUrl]) @@ -125,12 +162,8 @@ function useOnlineStatus({ async ({ type }: Event) => { type === 'online' ? _onlineStatusFn() - : setIsOnline({ - online: false, - time: { - since: new Date(), - diff: timeSince(new Date()) - } + : dispatch({ + type: 'offline' }) }, [_onlineStatusFn] @@ -148,12 +181,12 @@ function useOnlineStatus({ }, [handleOnlineStatus]) return { - attributes: { isOnline: isOnline.online }, + attributes: { isOnline: statusState.online }, connectionInfo, error: null, - isOffline: !isOnline.online, - isOnline: isOnline.online, - time: { since: isOnline.time.since, difference: isOnline.time.diff } + isOffline: !statusState.online, + isOnline: statusState.online, + time: { since: statusState.time.since, difference: statusState.time.diff } } } @@ -205,3 +238,26 @@ function useFirstRender(): { isFirstRender: boolean } { } export { useFirstRender, useOnlineStatus } + +type Megabit = number +type Millisecond = number +type EffectiveConnectionType = '2g' | '3g' | '4g' | 'slow-2g' +type ConnectionType = + | 'bluetooth' + | 'cellular' + | 'ethernet' + | 'mixed' + | 'none' + | 'other' + | 'unknown' + | 'wifi' + | 'wimax' +interface NetworkInformation extends EventTarget { + readonly type?: ConnectionType + readonly effectiveType?: EffectiveConnectionType + readonly downlinkMax?: Megabit + readonly downlink?: Megabit + readonly rtt?: Millisecond + readonly saveData?: boolean + onchange?: EventListener +} diff --git a/src/network-information-api.d.ts b/src/network-information-api.d.ts deleted file mode 100644 index 6b658ac..0000000 --- a/src/network-information-api.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -declare interface NavigatorNetworkInformation { - readonly connection?: NetworkInformation -} -type Megabit = number -type Millisecond = number -type EffectiveConnectionType = '2g' | '3g' | '4g' | 'slow-2g' -type ConnectionType = - | 'bluetooth' - | 'cellular' - | 'ethernet' - | 'mixed' - | 'none' - | 'other' - | 'unknown' - | 'wifi' - | 'wimax' -export interface NetworkInformation extends EventTarget { - readonly type?: ConnectionType - readonly effectiveType?: EffectiveConnectionType - readonly downlinkMax?: Megabit - readonly downlink?: Megabit - readonly rtt?: Millisecond - readonly saveData?: boolean - onchange?: EventListener -} diff --git a/src/nsn.ts b/src/nsn.ts index 011644b..7594d79 100644 --- a/src/nsn.ts +++ b/src/nsn.ts @@ -1,3 +1,2 @@ export { OnlineStatusNotification } from './OnlineStatusNotification' -export type { Position } from './OnlineStatusNotification' export { useOnlineStatus } from './hooks' diff --git a/src/utils.ts b/src/utils.ts index 8e23816..846c014 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,3 +31,5 @@ export function timeSince(date: Date) { return Math.floor(seconds) + ' seconds' } + +export const DEFAULT_POLLING_URL = 'https://www.gstatic.com/generate_204' diff --git a/tsconfig.json b/tsconfig.json index 6da02f5..2cf7305 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,5 +20,5 @@ "jsx": "react-jsx" }, "exclude": ["node_modules"], - "include": ["src"] + "include": ["./src"] } diff --git a/vite.config.ts b/vite.config.ts index 0605ada..24f433c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,17 +1,18 @@ -import * as packageJson from './package.json' import react from '@vitejs/plugin-react' import { resolve } from 'path' import { visualizer } from 'rollup-plugin-visualizer' import { PluginOption, defineConfig } from 'vite' import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' import dts from 'vite-plugin-dts' +import * as packageJson from './package.json' export default defineConfig({ plugins: [ react({ jsxRuntime: 'classic' }), cssInjectedByJsPlugin(), dts({ - insertTypesEntry: true + insertTypesEntry: true, + rollupTypes: true }), visualizer({ template: 'treemap', // or sunburst