bastienrobert/la-ferme

View on GitHub
packages/mobile/src/components/shared/NotificationBanner.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import React, { FC, useRef, useLayoutEffect, useCallback } from 'react'
import {
  Animated,
  TouchableWithoutFeedback,
  LayoutChangeEvent,
  LayoutRectangle,
  Easing
} from 'react-native'
import styled from 'styled-components/native'
import { Icon, Icons, Colors } from '@la-ferme/components/native'

import Container from './Container'
import SmallCirclesWrapper from './SmallCirclesWrapper'
import Title from '@/components/typo/Title'
import Text from '@/components/typo/Text'

export interface NotificationBannerProps {
  icon: Icons
  title: string
  subtitle: string
  onClose: () => void
}

const NotificationBanner: FC<NotificationBannerProps> = ({
  children,
  icon,
  title,
  subtitle,
  onClose
}) => {
  const layout = useRef<LayoutRectangle>()
  const locked = useRef<boolean>(false)
  const opacityBanner = useRef(new Animated.Value(1)).current
  const translateBanner = useRef(new Animated.ValueXY({ x: 0, y: -300 }))
    .current
  const translateLarge = useRef(new Animated.ValueXY({ x: 0, y: -12000 }))
    .current

  useLayoutEffect(() => {
    Animated.timing(translateBanner, {
      toValue: { x: 0, y: 0 },
      duration: 400,
      useNativeDriver: true
    }).start()
  }, [translateBanner])

  const onLayout = useCallback(
    (e: LayoutChangeEvent) => {
      layout.current = e.nativeEvent.layout
      translateLarge.setValue({ x: 0, y: -layout.current.height })
    },
    [translateLarge]
  )

  const onPress = useCallback(() => {
    if (locked.current) return
    Animated.timing(translateLarge, {
      toValue: { x: 0, y: 0 },
      duration: 600,
      easing: Easing.out(Easing.exp),
      useNativeDriver: true
    }).start()
  }, [locked, translateLarge])

  const onClosePress = useCallback(() => {
    locked.current = true
    Animated.parallel([
      Animated.timing(opacityBanner, {
        toValue: 0,
        duration: 0,
        easing: Easing.out(Easing.exp),
        useNativeDriver: true
      }),
      Animated.timing(translateLarge, {
        toValue: { x: 0, y: -layout.current.height },
        duration: 600,
        easing: Easing.out(Easing.exp),
        useNativeDriver: true
      })
    ]).start(({ finished }) => {
      if (!finished) return
      onClose()
    })
  }, [locked, onClose, opacityBanner, translateLarge])

  return (
    <TouchableWithoutFeedback onPress={onPress}>
      <Component
        as={Animated.View}
        onLayout={onLayout}
        style={{
          transform: [{ translateY: translateBanner.y }]
        }}>
        <InnerContainer>
          <BannerContainer
            as={Animated.View}
            style={{ opacity: opacityBanner }}>
            <SmallCirclesWrapper>
              <Icon icon={icon} />
            </SmallCirclesWrapper>
            <TextWrapper>
              <Title color="red" preset="H4">
                {title}
              </Title>
              <Text color="beige">{subtitle}</Text>
            </TextWrapper>
          </BannerContainer>
          <LargeContainer
            as={Animated.View}
            style={{
              transform: [{ translateY: translateLarge.y }]
            }}>
            {children}
            <Container alignSelf="center">
              <Icon icon="cross" background="red" onPress={onClosePress} />
            </Container>
          </LargeContainer>
        </InnerContainer>
      </Component>
    </TouchableWithoutFeedback>
  )
}

const Component = styled(Container)`
  position: absolute;
  padding: 11px;
  height: 100%;
  top: 23px;
  left: 0;
  width: 100%;
  box-shadow: 0px 4px 3px rgba(0, 0, 0, 0.35);
`

const InnerContainer = styled(Container)`
  flex: 1;
  flex-direction: row;
  align-items: flex-start;
  border-radius: 20px;
  padding-bottom: 20px;
  overflow: hidden;
`

const BannerContainer = styled(Container)`
  flex: 1;
  flex-direction: row;
  padding: 13px 21px;
  background-color: ${Colors.gray};
  border-radius: 20px;
`

const LargeContainer = styled(BannerContainer)`
  position: absolute;
  flex-direction: column;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 22px;
  background-color: ${Colors.gray};
`

const TextWrapper = styled(Container)`
  flex: 1;
  align-self: center;
  margin-left: 20px;
  background-color: ${Colors.gray};
`

export default NotificationBanner