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 ( - - -
- } - /> - - - - - Log in - - - - - Forgot password? - - - I don’t have an account - - - -
+ + + +
+ } + /> + + + + + Log in + + + + + Forgot password? + + + I don’t have an account + + + +
+
); }; 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"