>(
+ ({ 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 {};