From 7d263db8e329da21e7280a041156495703002c4a Mon Sep 17 00:00:00 2001 From: Emmanuel Onah <37575223+emmanuelonah@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:33:50 +0200 Subject: [PATCH] Update: updated... --- .storybook/preview.ts | 14 ----- .storybook/preview.tsx | 35 ++++++++++++ package.json | 4 +- .../async-renderer/index.component.tsx | 40 ++++++++++++++ .../async-renderer/index.stories.tsx | 24 +++++++++ .../components/async-renderer/index.test.tsx | 51 ++++++++++++++++++ .../error-text}/.gitkeep | 0 src/shared/components/index.ts | 2 + .../internet-notifier/index.stories.tsx | 7 ++- .../loader/base/index.component.tsx | 24 +++++++++ .../components/loader/base/useBaseLogic.ts | 53 +++++++++++++++++++ .../components/loader/gif/index.component.tsx | 15 ++++++ .../components/loader/gif/index.style.tsx | 41 ++++++++++++++ .../components/loader/index.component.tsx | 3 ++ .../components/loader/index.stories.tsx | 14 +++++ src/shared/components/loader/index.test.tsx | 9 ++++ .../components/loader/skeleton/.gitkeep | 0 .../loader/text/index.component.tsx | 14 +++++ .../components/loader/text/index.style.tsx | 16 ++++++ src/shared/services/index.ts | 1 + 20 files changed, 347 insertions(+), 20 deletions(-) delete mode 100644 .storybook/preview.ts create mode 100644 .storybook/preview.tsx create mode 100644 src/shared/components/async-renderer/index.component.tsx create mode 100644 src/shared/components/async-renderer/index.stories.tsx create mode 100644 src/shared/components/async-renderer/index.test.tsx rename src/shared/{services => components/error-text}/.gitkeep (100%) create mode 100644 src/shared/components/loader/base/index.component.tsx create mode 100644 src/shared/components/loader/base/useBaseLogic.ts create mode 100644 src/shared/components/loader/gif/index.component.tsx create mode 100644 src/shared/components/loader/gif/index.style.tsx create mode 100644 src/shared/components/loader/index.component.tsx create mode 100644 src/shared/components/loader/index.stories.tsx create mode 100644 src/shared/components/loader/index.test.tsx create mode 100644 src/shared/components/loader/skeleton/.gitkeep create mode 100644 src/shared/components/loader/text/index.component.tsx create mode 100644 src/shared/components/loader/text/index.style.tsx create mode 100644 src/shared/services/index.ts diff --git a/.storybook/preview.ts b/.storybook/preview.ts deleted file mode 100644 index adcda96..0000000 --- a/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Preview } from '@storybook/react'; - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..a4a8930 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { Preview } from '@storybook/react'; +import { ThemeProvider } from 'styled-components'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +import { GlobalStore } from '../src/global-store'; +import { theme, GlobalStyles } from '../src/design-system'; + +const preview: Preview = { + decorators: [ + (Story) => ( + + + + +
+ +
+
+
+
+ ), + ], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/package.json b/package.json index 4430c52..4790f73 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,8 @@ "test:cv": "yarn run test --coverage --watchAll=false", "lint": "eslint . --ext .ts --ext .tsx", "format": "prettier --write .", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", + "sb": "storybook dev -p 6006", + "build:sb": "storybook build", "pkgs:audit": " yarn audit --json > audit.json", "performance:audit": "lighthouse http://localhost:3000/", "pre:commit": "yarn run lint", diff --git a/src/shared/components/async-renderer/index.component.tsx b/src/shared/components/async-renderer/index.component.tsx new file mode 100644 index 0000000..59967fc --- /dev/null +++ b/src/shared/components/async-renderer/index.component.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { TextLoader } from 'shared/components'; + +type PrimitiveDivPropTypes = React.ComponentPropsWithoutRef<'div'>; +type AsyncRendererElement = React.ElementRef<'div'>; + +interface AsyncRendererPropTypes extends Omit { + isLoading?: boolean; + error?: Error | null; + data?: DataType | null; + loader?: React.ReactNode; + children?: ((data: DataType) => React.ReactNode) | React.ReactNode; +} + +export const AsyncRenderer = React.forwardRef>( + ({ isLoading, error, data, loader, children, ...rest }, forwardedRef) => { + if (isLoading) { + return ( +
+ {loader || } +
+ ); + } + + if (error) { + return ( +
+ {error.message} +
+ ); + } + + return ( +
+ {typeof children === 'function' ? children(data) : children} +
+ ); + } +); diff --git a/src/shared/components/async-renderer/index.stories.tsx b/src/shared/components/async-renderer/index.stories.tsx new file mode 100644 index 0000000..d2cd53c --- /dev/null +++ b/src/shared/components/async-renderer/index.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { StoryFn, Meta } from '@storybook/react'; + +import { AsyncRenderer } from './index.component'; + +export default { + title: 'Components/AsyncRenderer', + component: AsyncRenderer, +} as Meta; + +export const IsLoading: StoryFn = () => ; + +export const WithError: StoryFn = () => ( + +); + +export const WithJsxData: StoryFn = () => ( + Hello, World! +); + +export const WithFCCData: StoryFn = () => ( + {(data) => data.name} +); diff --git a/src/shared/components/async-renderer/index.test.tsx b/src/shared/components/async-renderer/index.test.tsx new file mode 100644 index 0000000..e9f639a --- /dev/null +++ b/src/shared/components/async-renderer/index.test.tsx @@ -0,0 +1,51 @@ +import { render, screen } from 'test'; + +import { AsyncRenderer } from './index.component'; + +describe('', () => { + it('should render loader when isLoading is true', () => { + render( + null} + /> + ); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('should render error message when error is not null', () => { + render( + null} + /> + ); + expect(screen.getByText('Error message')).toBeInTheDocument(); + }); + + it('should render children when isLoading is false and error is null', () => { + render( + data.name} + /> + ); + expect(screen.getByText('Foo Bar Baz')).toBeInTheDocument(); + }); + + it('should render children when isLoading is false and error is null', () => { + render( + + Children + + ); + expect(screen.getByText('Children')).toBeInTheDocument(); + }); +}); diff --git a/src/shared/services/.gitkeep b/src/shared/components/error-text/.gitkeep similarity index 100% rename from src/shared/services/.gitkeep rename to src/shared/components/error-text/.gitkeep diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts index b63aca8..99a23c9 100644 --- a/src/shared/components/index.ts +++ b/src/shared/components/index.ts @@ -1,3 +1,5 @@ +export * from './loader/index.component'; export * from './headings/index.component'; export * from './error-boundary/index.component'; +export * from './async-renderer/index.component'; export * from './internet-notifier/index.component'; diff --git a/src/shared/components/internet-notifier/index.stories.tsx b/src/shared/components/internet-notifier/index.stories.tsx index 6913cde..9a2cd72 100644 --- a/src/shared/components/internet-notifier/index.stories.tsx +++ b/src/shared/components/internet-notifier/index.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { StoryFn, Meta } from '@storybook/react'; + +import { Meta } from '@storybook/react'; import { InternetNotifier } from './index.component'; @@ -8,6 +9,4 @@ export default { component: InternetNotifier, } as Meta; -export const TurnOnAndOffYourWifiToSee: StoryFn = () => ( - -); +export const TurnOnAndOffYourWifiToSee = () => ; diff --git a/src/shared/components/loader/base/index.component.tsx b/src/shared/components/loader/base/index.component.tsx new file mode 100644 index 0000000..1da647d --- /dev/null +++ b/src/shared/components/loader/base/index.component.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { useBaseLogic } from './useBaseLogic'; + +type PrimitiveDivPropTypes = React.ComponentPropsWithoutRef<'div'>; +export type LoaderElement = React.ElementRef<'div'>; +export interface BasePropTypes extends PrimitiveDivPropTypes { + isLoading: boolean; + benefitOf?: number; +} + +export const Base = React.forwardRef(function Base( + { isLoading, benefitOf, children, ...restProps }, + forwardedRef +) { + const { isDoneLoading } = useBaseLogic({ isLoading, benefitOf }); + + return ( +
+
{children}
+ {isDoneLoading &&

Done Loading

} +
+ ); +}); diff --git a/src/shared/components/loader/base/useBaseLogic.ts b/src/shared/components/loader/base/useBaseLogic.ts new file mode 100644 index 0000000..360a590 --- /dev/null +++ b/src/shared/components/loader/base/useBaseLogic.ts @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; + +type ArgType = { + isLoading: boolean; + /** + * @benefitOf the maximum time in milliseconds if the + * side effect if not DoneLoading the loader will be shown + */ + benefitOf?: number; +}; + +export const useBaseLogic = ({ isLoading, benefitOf = 2000 }: ArgType) => { + const [loading, setLoading] = useState(false); + + /** + * @isDoneLoading is a state that will be set to true to notify + * the accessibility gadget users that loading is done + */ + const [isDoneLoading, setDoneLoading] = useState(false); + + /** + * Give the side effect a benefit of 20000ms to show the loader + */ + useEffect(() => { + let timer: NodeJS.Timeout; + if (isLoading) timer = setTimeout(() => setLoading(true), benefitOf); + return () => clearTimeout(timer); + }); + + /** + * Tell accessibility gadget users that loading is done + */ + useEffect(() => { + if (loading && !isLoading && !isDoneLoading) { + setLoading(false); + setDoneLoading(true); + } + }, [isDoneLoading, isLoading, loading]); + + /** + * Turn isDoneLoading to false after 2000ms + */ + useEffect(() => { + let timer: NodeJS.Timeout; + if (isDoneLoading) timer = setTimeout(() => setDoneLoading(false), 2000); + return () => clearTimeout(timer); + }); + + return { + loading, + isDoneLoading, + }; +}; diff --git a/src/shared/components/loader/gif/index.component.tsx b/src/shared/components/loader/gif/index.component.tsx new file mode 100644 index 0000000..11a12db --- /dev/null +++ b/src/shared/components/loader/gif/index.component.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { OuterRing, InnerRing } from './index.style'; +import { Base, LoaderElement, BasePropTypes } from '../base/index.component'; + +export const GifLoader = React.forwardRef( + function GifLoader(props, forwardedRef) { + return ( + + + + + ); + } +); diff --git a/src/shared/components/loader/gif/index.style.tsx b/src/shared/components/loader/gif/index.style.tsx new file mode 100644 index 0000000..2b48073 --- /dev/null +++ b/src/shared/components/loader/gif/index.style.tsx @@ -0,0 +1,41 @@ +import styled, { keyframes } from 'styled-components'; + +const rotate = keyframes` + 0% { + transform:rotate(0deg); + } + + 100% { + transform:rotate(360deg); + } +`; + +const OuterRing = styled.div` + position: absolute; + left: calc(50% - 150px); + height: 150px; + width: 150px; + border-radius: 50%; + background-image: linear-gradient( + 135deg, + #feed07 0%, + #fe6a50 5%, + #ed00aa 15%, + #2fe3fe 50%, + #8900ff 100% + ); + animation-duration: 2s; + animation-name: ${rotate}; + animation-iteration-count: infinite; +`; + +const InnerRing = styled.div` + position: absolute; + left: calc(50% - 140px); + height: 140px; + width: 140px; + border-radius: 50%; + background-image: linear-gradient(0deg, #36295e, #1c1045); +`; + +export { OuterRing, InnerRing }; diff --git a/src/shared/components/loader/index.component.tsx b/src/shared/components/loader/index.component.tsx new file mode 100644 index 0000000..93a978a --- /dev/null +++ b/src/shared/components/loader/index.component.tsx @@ -0,0 +1,3 @@ +export * from './base/index.component'; +export * from './gif/index.component'; +export * from './text/index.component'; diff --git a/src/shared/components/loader/index.stories.tsx b/src/shared/components/loader/index.stories.tsx new file mode 100644 index 0000000..73e3e7a --- /dev/null +++ b/src/shared/components/loader/index.stories.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Meta } from '@storybook/react'; + +import * as Loaders from './index.component'; + +export default { + title: 'Components/Loaders', + component: Loaders.Base, +} as Meta; + +export const TextLoader = () => ; + +export const GifLoader = () => ; diff --git a/src/shared/components/loader/index.test.tsx b/src/shared/components/loader/index.test.tsx new file mode 100644 index 0000000..130fe52 --- /dev/null +++ b/src/shared/components/loader/index.test.tsx @@ -0,0 +1,9 @@ +import * as Loaders from './index.component'; + +describe('', () => { + describe('', () => {}); + + describe('', () => {}); + + describe('', () => {}); +}); diff --git a/src/shared/components/loader/skeleton/.gitkeep b/src/shared/components/loader/skeleton/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/components/loader/text/index.component.tsx b/src/shared/components/loader/text/index.component.tsx new file mode 100644 index 0000000..bed1647 --- /dev/null +++ b/src/shared/components/loader/text/index.component.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Text } from './index.style'; +import { Base, LoaderElement, BasePropTypes } from '../base/index.component'; + +export const TextLoader = React.forwardRef( + function TextLoader(props, forwardedRef) { + return ( + + Loading... + + ); + } +); diff --git a/src/shared/components/loader/text/index.style.tsx b/src/shared/components/loader/text/index.style.tsx new file mode 100644 index 0000000..d5e0191 --- /dev/null +++ b/src/shared/components/loader/text/index.style.tsx @@ -0,0 +1,16 @@ +import styled, { keyframes } from 'styled-components'; + +const dim = keyframes` + 0% { + color: inherit; + } + + 100% { + color: hsl(200, 20%, 95%); + } +`; + +export const Text = styled.p` + opacity: 0.7; + animation: ${dim} 1s linear infinite alternate; +`; diff --git a/src/shared/services/index.ts b/src/shared/services/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/shared/services/index.ts @@ -0,0 +1 @@ +export {};