diff --git a/packages/client/package.json b/packages/client/package.json
index f3d73451..d26eadf3 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -24,6 +24,7 @@
"axios": "^0.21.1",
"i18next": "^19.9.0",
"i18next-browser-languagedetector": "^6.0.1",
+ "intersection-observer": "^0.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-hook-form": "^6.14.1",
diff --git a/packages/client/src/containers/Login/index.tsx b/packages/client/src/containers/Login/index.tsx
index 09136410..b34d9509 100644
--- a/packages/client/src/containers/Login/index.tsx
+++ b/packages/client/src/containers/Login/index.tsx
@@ -3,6 +3,7 @@ import { useForm } from 'react-hook-form';
import { useHistory } from 'react-router-dom';
import {
+ Animated,
AnimatedLogo,
BackgroundSideLogo,
Checkbox,
@@ -62,44 +63,46 @@ const Login = () => {
return (
-
-
-
-
+
+
+
+
+
+
);
};
diff --git a/packages/client/src/elements/Animated/Animated.stories.tsx b/packages/client/src/elements/Animated/Animated.stories.tsx
new file mode 100644
index 00000000..9b4266a6
--- /dev/null
+++ b/packages/client/src/elements/Animated/Animated.stories.tsx
@@ -0,0 +1,17 @@
+import { Meta, Story } from '@storybook/react/types-6-0';
+import React from 'react';
+
+import Animated from '.';
+import { AnimatedProps } from './Animated.types';
+
+export default {
+ title: 'Elements/Animated',
+ component: Animated,
+} as Meta;
+
+const Template: Story = (args) => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ children: <>Lorem Ipsum>,
+};
diff --git a/packages/client/src/elements/Animated/Animated.types.d.ts b/packages/client/src/elements/Animated/Animated.types.d.ts
new file mode 100644
index 00000000..572d7671
--- /dev/null
+++ b/packages/client/src/elements/Animated/Animated.types.d.ts
@@ -0,0 +1,7 @@
+import { ReactNode } from 'react';
+
+export interface AnimatedProps {
+ children?: ReactNode;
+ animateIn?: string;
+ animateOut?: string;
+}
diff --git a/packages/client/src/elements/Animated/index.tsx b/packages/client/src/elements/Animated/index.tsx
new file mode 100644
index 00000000..1387738b
--- /dev/null
+++ b/packages/client/src/elements/Animated/index.tsx
@@ -0,0 +1,73 @@
+import anime, { AnimeInstance } from 'animejs';
+import React, { useLayoutEffect, useRef } from 'react';
+import styled from 'styled-components';
+
+import { useOnScreen } from '../../hooks';
+import { AnimatedProps } from './Animated.types';
+
+const Wrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+`;
+
+const Animated = ({ children, animateIn, animateOut }: AnimatedProps) => {
+ const animatedContainerRef = useRef(null);
+ const isInViewPort = useOnScreen(animatedContainerRef);
+ const animateInRef = useRef();
+ const animateOutRef = useRef();
+
+ useLayoutEffect(() => {
+ // const xMax = 16;
+ // animateInRef.current = anime({
+ // targets: animatedContainerRef.current,
+ // easing: 'easeInOutSine',
+ // duration: 550,
+ // translateX: [
+ // {
+ // value: xMax * -1,
+ // },
+ // {
+ // value: xMax,
+ // },
+ // {
+ // value: xMax / -2,
+ // },
+ // {
+ // value: xMax / 2,
+ // },
+ // {
+ // value: 0,
+ // },
+ // ],
+ // });
+ if (isInViewPort) {
+ animateInRef.current = anime({
+ targets: animatedContainerRef.current,
+ duration: 750,
+ opacity: [0, 1],
+ easing: 'cubicBezier(.5, .05, .1, .3)',
+ });
+ }
+ if (!isInViewPort) {
+ animateOutRef.current = anime({
+ targets: animatedContainerRef.current,
+ duration: 750,
+ opacity: [1, 0],
+ easing: 'cubicBezier(.5, .05, .1, .3)',
+ });
+ }
+ }, [isInViewPort]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+Animated.displayName = 'Animated';
+
+export default Animated;
diff --git a/packages/client/src/elements/index.ts b/packages/client/src/elements/index.ts
index 2435507a..2400f534 100644
--- a/packages/client/src/elements/index.ts
+++ b/packages/client/src/elements/index.ts
@@ -1,3 +1,4 @@
+export { default as Animated } from './Animated';
export { default as AnimatedLogo } from './AnimatedLogo';
export { default as BackgroundSideLogo } from './BackgroundSideLogo';
export { default as Button } from './Button';
diff --git a/packages/client/src/hooks/index.ts b/packages/client/src/hooks/index.ts
index deba5df3..ec144a29 100644
--- a/packages/client/src/hooks/index.ts
+++ b/packages/client/src/hooks/index.ts
@@ -4,5 +4,6 @@ export { default as useHotkeys } from './useHotkeys';
export { default as useMediaDevice } from './useMediaDevice';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useOnClickOutside } from './useOnClickOutside';
+export { default as useOnScreen } from './useOnScreen';
export { default as useSnackbar } from './useSnackbar';
export { default as useThemeType } from './useThemeType';
diff --git a/packages/client/src/hooks/useOnScreen.ts b/packages/client/src/hooks/useOnScreen.ts
new file mode 100644
index 00000000..b8fe53a2
--- /dev/null
+++ b/packages/client/src/hooks/useOnScreen.ts
@@ -0,0 +1,35 @@
+import { RefObject, useLayoutEffect, useState } from 'react';
+
+const useOnScreen = (ref: RefObject, rootMargin = '0px') => {
+ const [isIntersecting, setIntersecting] = useState(true);
+
+ useLayoutEffect(() => {
+ if (!ref && !window && !('IntersectionObserver' in window)) {
+ return;
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ setIntersecting(entry.isIntersecting);
+ },
+ {
+ rootMargin,
+ },
+ );
+
+ if (ref.current) {
+ observer.observe(ref.current);
+ }
+
+ return () => {
+ if (ref.current) {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ observer.unobserve(ref.current);
+ }
+ };
+ }, [ref, rootMargin]);
+
+ return isIntersecting;
+};
+
+export default useOnScreen;
diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx
index 502ca8ba..43cca6e2 100644
--- a/packages/client/src/index.tsx
+++ b/packages/client/src/index.tsx
@@ -6,6 +6,13 @@ import Routes from './Routes';
import * as serviceWorkerRegistration from './serviceWorker/serviceWorkerRegistration';
import reportWebVitals from './utils/reportWebVitals';
+// Load Polyfills
+(async () => {
+ if (typeof window.IntersectionObserver === 'undefined') {
+ await import('intersection-observer');
+ }
+})();
+
ReactDOM.render(
diff --git a/packages/client/src/interfaces/intersection-observer.d.ts b/packages/client/src/interfaces/intersection-observer.d.ts
new file mode 100644
index 00000000..bafd1669
--- /dev/null
+++ b/packages/client/src/interfaces/intersection-observer.d.ts
@@ -0,0 +1 @@
+declare module 'intersection-observer';
diff --git a/yarn.lock b/yarn.lock
index e8a502c6..0e0dcd94 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10758,6 +10758,11 @@ interpret@^2.0.0:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+intersection-observer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.0.tgz#6c84628f67ce8698e5f9ccf857d97718745837aa"
+ integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==
+
invariant@^2.2.3, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"