react-intersection-observer#useInView TypeScript Examples

The following examples show how to use react-intersection-observer#useInView. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: animated-counter.tsx    From web with Apache License 2.0 7 votes vote down vote up
AnimatedCounter = ({ countTo }: PropTypes) => {
  const [ref, inView] = useInView({ delay: 300, triggerOnce: true })

  return (
    <div ref={ref}>
      {inView ? (
        <CountUp
          delay={0}
          start={0}
          end={countTo}
          useEasing
          duration={3}
          formattingFn={(v) => numeral(v).format('0.0a')}
        >
          {({ countUpRef }) => <span ref={countUpRef} />}
        </CountUp>
      ) : (
        <span>0</span>
      )}
    </div>
  )
}
Example #2
Source File: useInViewAnimate.ts    From framer-motion-hooks with MIT License 6 votes vote down vote up
useInViewAnimate = (
  { initial, animate }: IStates,
  options: IntersectionOptions = {}
) => {
  const animation = useAnimation()

  const [inViewRef, inView] = useInView(options)

  useEffect(() => {
    if (initial) animation.set(initial)
  }, [])

  useEffect(() => {
    if (inView) {
      animation.start(animate)
    } else if (initial && options.triggerOnce === false) {
      animation.start(initial)
    }
  }, [inView])

  return { inViewRef, animation }
}
Example #3
Source File: ab-animation.tsx    From basement-grotesque with SIL Open Font License 1.1 5 votes vote down vote up
AbAnimation = () => {
  const { ref, inView } = useInView({ threshold: 0.1, triggerOnce: true })
  const tlRef = useRef<gsap.core.Timeline>()

  useEffect(() => {
    gsap.set('.ab svg', { autoAlpha: 0 })
  }, [])

  useEffect(() => {
    if (!inView) return

    function handleResize() {
      const multiplier = Math.min((window.innerWidth * 20) / 1440, 20)
      const tl = gsap.timeline({
        paused: true,
        smoothChildTiming: true
      })
      tl.to('.ab svg', {
        delay: DURATION / 2,
        y: (index) => index * multiplier,
        x: (index) => index * multiplier,
        autoAlpha: 1,
        ease: 'elastic.out(1, 0.75)',
        duration: DURATION,
        stagger: {
          each: 0.02,
          from: 'end'
        }
      })
        .timeScale(0.4)
        .play()
      tlRef.current = tl
    }

    handleResize()
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [inView])

  useEffect(() => {
    return () => {
      // eslint-disable-next-line react-hooks/exhaustive-deps
      tlRef.current?.kill()
    }
  }, [])

  return (
    <Box
      css={{
        position: 'relative',
        width: '100%',
        marginTop: '-64px',
        '.absolute': { position: 'absolute', inset: 0 }
      }}
      ref={ref}
    >
      <Ab1 />
      <Ab2 />
      <Ab3 />
      <Ab4 />
    </Box>
  )
}
Example #4
Source File: useTrackImpression.ts    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
export default function useTrackImpression(
  item: FeedItem,
  index: number,
  columns: number,
  column: number,
  row: number,
  feedName: string,
  ranking?: string,
): (node?: Element | null) => void {
  const { trackEventStart, trackEventEnd } = useContext(AnalyticsContext);
  const { ref: inViewRef, inView } = useInView({
    threshold: 0.5,
  });

  useEffect(() => {
    if (item.type === 'post') {
      const eventKey = `pi-${item.post.id}`;
      if (inView && !item.post.impressionStatus) {
        trackEventStart(
          eventKey,
          postAnalyticsEvent('impression', item.post, {
            columns,
            column,
            row,
            ...feedAnalyticsExtra(feedName, ranking, {
              scroll_y: window.scrollY,
            }),
          }),
        );
        // eslint-disable-next-line no-param-reassign
        item.post.impressionStatus = TRACKING;
      } else if (!inView && item.post.impressionStatus === TRACKING) {
        trackEventEnd(eventKey);
        // eslint-disable-next-line no-param-reassign
        item.post.impressionStatus = TRACKED;
      }
    } else if (item.type === 'ad') {
      const eventKey = `ai-${index}`;
      if (inView && !item.ad.impressionStatus) {
        trackEventStart(
          eventKey,
          adAnalyticsEvent('impression', item.ad, {
            columns,
            column,
            row,
            ...feedAnalyticsExtra(feedName, ranking),
          }),
        );
        // eslint-disable-next-line no-param-reassign
        item.ad.impressionStatus = TRACKING;
      } else if (!inView && item.ad.impressionStatus === TRACKING) {
        trackEventEnd(eventKey);
        // eslint-disable-next-line no-param-reassign
        item.ad.impressionStatus = TRACKED;
      }
    }
  }, [inView]);

  useEffect(() => {
    // Send pending impression on unmount
    return () => {
      if (item.type === 'ad' && item.ad.impressionStatus === TRACKING) {
        const eventKey = `ai-${index}`;
        trackEventEnd(eventKey);
        // eslint-disable-next-line no-param-reassign
        item.ad.impressionStatus = TRACKED;
      } else if (
        item.type === 'post' &&
        item.post.impressionStatus === TRACKING
      ) {
        const eventKey = `pi-${item.post.id}`;
        trackEventEnd(eventKey);
        // eslint-disable-next-line no-param-reassign
        item.post.impressionStatus = TRACKED;
      }
    };
  }, []);

  return inViewRef;
}
Example #5
Source File: useFeedInfiniteScroll.ts    From apps with GNU Affero General Public License v3.0 5 votes vote down vote up
export default function useFeedInfiniteScroll({
  fetchPage,
  canFetchMore,
}: UseFeedInfiniteScrollProps): (node?: Element | null) => void {
  const { ref: infiniteScrollRef, inView } = useInView({
    rootMargin: '20px',
    threshold: 1,
  });

  useEffect(() => {
    if (inView && canFetchMore) {
      fetchPage();
    }
  }, [inView, canFetchMore]);

  return infiniteScrollRef;
}
Example #6
Source File: rome-preview.tsx    From basement-grotesque with SIL Open Font License 1.1 4 votes vote down vote up
RomePreview = () => {
  const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })

  useEffect(() => {
    gsap.set('.rome-preview__section', {
      autoAlpha: 0
    })
  }, [])

  useEffect(() => {
    if (!inView) return

    const title = new SplitText('.rome-preview__title', {
      type: 'lines,words,chars'
    })

    const subtitle = new SplitText('.rome-preview__subtitle', {
      type: 'lines,words,chars'
    })

    const items = document.querySelectorAll('.rome-preview__items')[0]
      .childNodes

    const tl = gsap.timeline({
      paused: true,
      smoothChildTiming: true
    })
    tl.to('.rome-preview__section', {
      autoAlpha: 1,
      duration: DURATION / 2
    })
    tl.in(title.words)
    tl.from(
      '.rome-preview__svg',
      {
        autoAlpha: 0,
        y: 30
      },
      '< 30%'
    )
    tl.in(subtitle.lines, '>-30%')
    tl.from(items, {
      autoAlpha: 0,
      y: 30,
      stagger: 0.1
    })

    tl.timeScale(1.2).play()

    return () => {
      tl.kill()
    }
  }, [inView])

  return (
    <Section background="black" css={{ pt: '64px' }}>
      <Container
        className="rome-preview__section"
        css={{ maxWidth: '1800px', mx: 'auto' }}
        ref={ref}
      >
        <Text size="icon" centered className="rome-preview__svg">
          †
        </Text>
        <ContentContainer size="sm" centered>
          <Text
            className="rome-preview__title"
            size="lg"
            css={{ marginTop: 20, textTransform: 'uppercase', lineHeight: 1.2 }}
            centered
          >
            Lëtzebuerg
            <br />
            Moscow
            <br />
            Genève
            <br />
            Roma
          </Text>
        </ContentContainer>
        <Text
          size="icon"
          css={{ marginTop: 24 }}
          centered
          className="rome-preview__svg"
        >
          ‡
        </Text>
        <Divisor />
        <ContentContainer size="lg" centered>
          <ContentContainer size="sm">
            <Text size="md" className="rome-preview__subtitle">
              CATACOMBS, ALTHOUGH MOST NOTABLE AS UNDERGROUND PASSAGEWAYS AND
              CEMETERIES, ALSO HOUSE MANY DECORATIONS.
            </Text>
          </ContentContainer>
          <ColumnedContent
            css={{ marginTop: 80 }}
            className="rome-preview__items"
          >
            <ContentContainer
              css={{
                background: '$background',
                color: '#FDFDFD',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                maxHeight: 168
              }}
            >
              <span>
                <svg
                  width="119"
                  height="168"
                  viewBox="0 0 119 168"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M55.9082 91.3575C56.2204 97.9133 49.9768 106.342 38.1141 109.464L25.0027 107.279C-5.59058 101.972 -11.8341 96.9767 -11.8341 85.4262C-12.1463 78.8705 -5.90276 70.4417 5.95995 67.32L19.0714 69.5052C49.6647 74.8122 55.9082 79.807 55.9082 91.3575ZM-74.2694 94.7915C-74.2694 113.834 -67.0893 130.692 -37.1204 139.433C-21.1994 144.115 -6.83928 147.861 26.8758 152.856C47.1673 155.978 55.9082 159.724 55.9082 171.899C55.9082 186.884 52.7864 188.444 23.4418 188.444C-6.83928 188.444 -10.2732 185.947 -10.2732 156.915H-69.5868C-69.5868 215.604 -50.544 229.027 26.2514 229.027C99.9251 229.027 118.343 217.165 118.343 169.402C118.343 144.428 111.788 130.38 86.1893 120.702C83.0675 119.453 79.3214 118.205 75.5753 117.268C106.481 116.956 118.343 106.342 118.343 81.9922C118.343 62.9495 111.163 46.0919 81.1945 37.351C65.2735 32.6684 50.9134 28.9222 17.1983 23.9274C-3.09317 20.8056 -11.8341 17.0595 -11.8341 4.88465C-11.8341 -10.0998 -8.71234 -11.6607 20.6322 -11.6607C50.9134 -11.6607 54.3473 -9.16329 54.3473 19.8691H113.661C113.661 -38.82 94.6181 -52.2437 17.8227 -52.2437C-55.851 -52.2437 -74.2694 -40.3809 -74.2694 7.38207C-74.2694 32.3562 -67.7137 46.4041 -42.1152 56.0816C-38.9935 57.3303 -35.2473 58.579 -31.5012 59.5155C-63.3432 59.8277 -74.2694 70.4417 -74.2694 94.7915Z"
                    fill="#FDFDFD"
                  />
                </svg>
              </span>
              <Text css={{ fontSize: '$13', lineHeight: 1, marginRight: 24 }}>
                ¶
              </Text>
            </ContentContainer>
            <div>
              <Text size="sm">
                There are thousands of decorations in the centuries-old
                catacombs of Rome, catacombs of Paris, and other known and
                unknown catacombs, some of which include inscriptions,
                paintings, statues, ornaments, and other items placed in the
                graves over the years.
              </Text>
            </div>
          </ColumnedContent>
        </ContentContainer>
      </Container>
      <Container
        css={{ background: '$background', '@bp1': { background: 'black' } }}
        maxWidth
      >
        <Container
          css={{
            mt: '64px',
            background: '$background',
            padding: 0,
            '@bp1': {
              padding: `min(40px, ${toVw(40)})`
            }
          }}
        >
          <Box
            css={{
              display: 'grid',
              '@bp1': { gridTemplateColumns: '1fr 1fr' }
            }}
          >
            {[
              {
                label: (
                  <>
                    MANY ACCENTS,
                    <br />
                    OBVIOUSLY
                  </>
                ),
                children: (
                  <Box data-scroll data-scroll-speed={0.35}>
                    <span>
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        a
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        à
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.3}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        á
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.4}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        â
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.5}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        ä
                      </Box>
                    </span>
                    <br />
                    <span className="ss01">
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        a
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        à
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.3}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        á
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.4}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        â
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.5}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        ä
                      </Box>
                    </span>
                    <br />
                    <span className="ss02">
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        a
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        à
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.3}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        á
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.4}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        â
                      </Box>
                      <Box
                        data-scroll
                        data-scroll-speed={0.5}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        ä
                      </Box>
                    </span>
                  </Box>
                )
              },
              {
                label: (
                  <>
                    CAPITAL VARIABLES,
                    <br />
                    BECAUSE, WHY NOT?
                  </>
                ),
                background: 'white',
                color: 'black',
                invertSelection: true,
                children: (
                  <>
                    <span>
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        R
                      </Box>
                    </span>
                    <span className="ss01">
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        R
                      </Box>
                    </span>
                    <span className="ss02">
                      <Box
                        data-scroll
                        data-scroll-speed={0.3}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        R
                      </Box>
                    </span>
                    <span className="ss03">
                      <Box
                        data-scroll
                        data-scroll-speed={0.4}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        R
                      </Box>
                    </span>
                    <br />
                    <span>
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        G
                      </Box>
                    </span>
                    <span className="ss01">
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        G
                      </Box>
                    </span>
                    <br />
                    <span>
                      <Box
                        data-scroll
                        data-scroll-speed={0.1}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        Q
                      </Box>
                    </span>
                    <span className="ss01">
                      <Box
                        data-scroll
                        data-scroll-speed={0.2}
                        css={{ display: 'inline-block' }}
                        as="span"
                      >
                        Q
                      </Box>
                    </span>
                  </>
                )
              },
              {
                label: null,
                hideOnDesktop: true,
                children: (
                  <>
                    <span>1918-2021</span>
                    <br />
                    <span>£ 206.10</span>
                    <br />
                    <span>€ 37,00</span>
                  </>
                )
              }
            ].map(
              (
                {
                  label,
                  background,
                  color,
                  children,
                  invertSelection,
                  hideOnDesktop
                },
                i
              ) => {
                return (
                  <StyledAspectBox
                    key={i}
                    ratio={680 / 587}
                    className={invertSelection ? 'invert-selection' : ''}
                    css={{
                      background,
                      color,
                      position: 'relative',
                      label: {
                        position: 'absolute',
                        top: `min(24px, ${toVw(24)})`,
                        right: `min(32px, ${toVw(32)})`,
                        textAlign: 'right',
                        fontWeight: 500,
                        lineHeight: 1.25,
                        userSelect: 'none',
                        fontSize: `min(16px, ${toVw(16)})`,
                        display: 'none'
                      },
                      '@bp1': {
                        display: hideOnDesktop ? 'none' : 'block',
                        label: {
                          display: 'block'
                        }
                      }
                    }}
                  >
                    <label>{label}</label>
                    <Box
                      className="children"
                      css={{
                        width: '100%',
                        position: 'absolute',
                        top: '50%',
                        left: '50%',
                        transform: 'translate(-50%, -50%)',
                        fontFamily: '$heading',
                        letterSpacing: '0.04em',
                        lineHeight: 1,
                        textAlign: 'center',
                        fontSize: `min(56px, ${toVw(52, 375)})`,
                        '@bp1': {
                          fontSize: `min(112px, ${toVw(112)})`
                        }
                      }}
                    >
                      {children}
                    </Box>
                  </StyledAspectBox>
                )
              }
            )}
          </Box>
          <Box
            css={{
              display: 'none',
              textAlign: 'center',
              py: `min(120px, ${toVw(120)})`,
              mt: `min(40px, ${toVw(40)})`,
              label: {
                fontWeight: 500,
                lineHeight: 1.25,
                userSelect: 'none',
                mt: '24px',
                fontSize: `min(16px, ${toVw(16)})`,
                display: 'none'
              },
              '@bp1': {
                display: 'block',
                label: {
                  display: 'block'
                }
              }
            }}
          >
            <Box
              css={{
                fontFamily: '$heading',
                letterSpacing: '0.04em',
                lineHeight: 1,
                fontSize: `min(56px, ${toVw(55, 375)})`,
                '@bp1': {
                  fontSize: `min(112px, ${toVw(112)})`
                }
              }}
            >
              <Box data-scroll data-scroll-speed={0.1}>
                <span>1918-2021</span>
              </Box>
              <Box data-scroll data-scroll-speed={0.4}>
                <span>£ 206.10</span>
              </Box>
              <Box data-scroll data-scroll-speed={0.7}>
                <span>€ 37,00</span>
              </Box>
            </Box>
            <label data-scroll data-scroll-speed={0.4}>
              WHO NEEDS NUMBERS?
            </label>
          </Box>
        </Container>
      </Container>
    </Section>
  )
}
Example #7
Source File: Testing.tsx    From mswjs.io with MIT License 4 votes vote down vote up
Testing = () => {
  const [isLoading, setLoading] = useState(true)
  const [ref, isContainerVisible] = useInView({
    threshold: 1,
  })

  useEffect(() => {
    if (isContainerVisible) {
      const timeout = setTimeout(() => {
        setLoading(false)
      }, 1000)

      return () => {
        clearTimeout(timeout)
      }
    }
  }, [isContainerVisible])

  return (
    <ObliqueSection>
      <Section>
        <Box width="100%">
          <Box ref={ref} marginBottom={24}>
            <Browser
              address="eshop.com/product/1"
              maxWidth={500}
              marginHorizontal="auto"
            >
              <Box
                flex
                heightMd={250}
                padding={32}
                alignItems="center"
                justifyContent="center"
              >
                <PageContent isLoading={isLoading} />
              </Box>
              <Box as={BrowserDevTools} padding={20}>
                <TestResults>
                  <TestDescribe>
                    given I wrote a test suite with MSW
                  </TestDescribe>
                  <TestResult
                    title="treats API response as a pre-requisite"
                    isLoading={isLoading}
                  />
                  <TestResult
                    title="tests how a user actually interacts with my app"
                    isLoading={isLoading}
                    delay={850}
                  />
                  <TestResult
                    title="produces a maintainable and resilient test"
                    isLoading={isLoading}
                    delay={1700}
                  />
                </TestResults>
              </Box>
            </Browser>
          </Box>
          <TextSmall align="center" color="gray">
            Test suite using a <code>GET /product/:productId</code> mock.
          </TextSmall>
        </Box>
        <SectionContent>
          <Heading level={2} marginBottom={8} align="center" alignLg="start">
            Test with confidence
          </Heading>
          <TextLead align="center" alignLg="start">
            Write test suites that <Accent>don't smell</Accent>.
          </TextLead>
          <Text color="gray">
            You don't expect your customers to mock <code>fetch</code>, do you?
            So don't expect your tests either. Target any state of your API
            while testing your application exactly how your users interact with
            it.
          </Text>
          <ReadmoreLink
            href="https://kentcdodds.com/blog/stop-mocking-fetch"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn about API mocking in tests with Kent C. Dodds
          </ReadmoreLink>
        </SectionContent>
      </Section>
    </ObliqueSection>
  )
}
Example #8
Source File: useArt.ts    From metaplex with Apache License 2.0 4 votes vote down vote up
useExtendedArt = (id?: StringPublicKey) => {
  const { metadata } = useMeta();

  const [data, setData] = useState<IMetadataExtension>();
  const { width } = useWindowDimensions();
  const { ref, inView } = useInView({ root: null, rootMargin: '-100px 0px' });
  const localStorage = useLocalStorage();

  const key = pubkeyToString(id);

  const account = useMemo(
    () => metadata.find(a => a.pubkey === key),
    [key, metadata],
  );

  useEffect(() => {
    if ((inView || width < 768) && id && !data) {
      const USE_CDN = false;
      const routeCDN = (uri: string) => {
        let result = uri;
        if (USE_CDN) {
          result = uri.replace(
            'https://arweave.net/',
            'https://coldcdn.com/api/cdn/bronil/',
          );
        }

        return result;
      };

      if (account && account.info.data.uri) {
        const uri = routeCDN(account.info.data.uri);

        const processJson = (extended: any) => {
          if (!extended || extended?.properties?.files?.length === 0) {
            return;
          }

          if (extended?.image) {
            const file = extended.image.startsWith('http')
              ? extended.image
              : `${account.info.data.uri}/${extended.image}`;
            extended.image = routeCDN(file);
          }

          return extended;
        };

        try {
          const cached = localStorage.getItem(uri);
          if (cached) {
            setData(processJson(JSON.parse(cached)));
          } else {
            // TODO: BL handle concurrent calls to avoid double query
            fetch(uri)
              .then(async _ => {
                try {
                  const data = await _.json();
                  try {
                    localStorage.setItem(uri, JSON.stringify(data));
                  } catch {
                    // ignore
                  }
                  setData(processJson(data));
                } catch {
                  return undefined;
                }
              })
              .catch(() => {
                return undefined;
              });
          }
        } catch (ex) {
          console.error(ex);
        }
      }
    }
  }, [inView, id, data, setData, account]);

  return { ref, data };
}
Example #9
Source File: Page.tsx    From react-pdf-ner-annotator with MIT License 4 votes vote down vote up
Page = ({
	pageNumber,
	shouldRender,
	page,
	scale,
	annotations,
	addAnnotation,
	updateLastAnnotationForEntity,
	addPageToTextMap,
	initialTextLayer,
}: Props) => {
	const {
		config: { disableOCR },
	} = useContext(ConfigContext);
	const { tokenizer } = useContext(AnnotationContext);

	const [inViewRef, inView] = useInView({ threshold: 0 });

	const canvasRef = useRef<HTMLCanvasElement>(null);

	const [loading, setLoading] = useState(true);
	const [pdfPage, setPdfPage] = useState<PDFPageProxy | null>(null);
	const [context, setContext] = useState<CanvasRenderingContext2D | null>(null);
	const [startOcr, setStartOcr] = useState(false);
	const [pageViewport, setPageViewport] = useState<any>({ width: (916 / 1.5) * scale, height: (1174 / 1.5) * scale });

	const { textLayer, buildTextLayer } = useTextLayer(scale, context!, initialTextLayer);
	const { ocrResult, ocrError, ocrLoading, doOCR } = useTesseract(scale, context!);

	const message = ocrResult ? `OCR confidence ${ocrResult.confidence}%` : undefined;

	useEffect(() => {
		if (annotations.length) {
			if (textLayer) {
				addPageToTextMap(pageNumber, textLayer, TextLayerType.TEXT_LAYER, 1, tokenizer);
				return;
			}
			if (ocrResult) {
				addPageToTextMap(pageNumber, ocrResult.ocrWords, TextLayerType.ORC, ocrResult.confidence);
			}
		}
	}, [annotations, textLayer, ocrResult, pageNumber, addPageToTextMap, tokenizer]);

	useEffect(() => {
		if (!disableOCR && startOcr && inView && !ocrResult) {
			doOCR();
		}
	}, [disableOCR, startOcr, inView, doOCR, ocrResult]);

	useEffect(() => {
		if (canvasRef) {
			setContext(canvasRef.current!.getContext('2d'));
		}
	}, [canvasRef]);

	useEffect(() => {
		if (canvasRef && context && page && inView) {
			page.then((pdfPageResult) => {
				const viewport = pdfPageResult.getViewport({ scale });
				const { width, height } = viewport;
				setPageViewport(viewport);
				const canvas = canvasRef.current;
				canvas!.width = width;
				canvas!.height = height;

				pdfPageResult
					.render({
						canvasContext: context!,
						viewport,
					})
					.promise.then(() => {
						setPdfPage(pdfPageResult);
					});
			});
		}
	}, [page, scale, canvasRef, context, inView]);

	useEffect(() => {
		if (textLayer?.length) {
			setLoading(false);
			return;
		}
		if (inView && pdfPage && !textLayer) {
			pdfPage.getTextContent().then((content) => {
				if (content.items.length) {
					const contentMerged = mergeSplitWords(content);
					buildTextLayer(contentMerged, pageViewport as PDFPageViewport);
				} else {
					setStartOcr(true);
				}
				setLoading(false);
			});
		}
	}, [inView, pdfPage, pageViewport, context, page, textLayer, buildTextLayer]);

	return (
		<div className="page" ref={inViewRef}>
			<div className="page__container" style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}>
				<div
					className="page__canvas-container"
					style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}
				>
					{loading ? <Loader /> : null}
					<canvas ref={canvasRef} style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }} />
				</div>
				<Selection
					pageNumber={pageNumber}
					className="page__text-layer-container"
					style={{ width: `${pageViewport.width}px`, height: `${pageViewport.height}px` }}
					addAnnotation={addAnnotation}
					updateLastAnnotationForEntity={updateLastAnnotationForEntity}
					pdfInformation={{ width: pageViewport.width, height: pageViewport.height, scale }}
					pdfContext={context}
				>
					<TextLayer
						inView={inView}
						shouldRender={shouldRender}
						canvasInitialized={!!canvasRef}
						textLayer={textLayer || ocrResult?.ocrWords}
						pageNumber={pageNumber}
						needsTokenization={!initialTextLayer}
					/>
					<AreaLayer pdfScale={scale} pageNumber={pageNumber} />
					<div className="ocr-info-container">
						<OcrInfo loading={ocrLoading} message={message} error={ocrError} />
					</div>
				</Selection>
			</div>
		</div>
	);
}
Example #10
Source File: Wrapper.tsx    From yugong with MIT License 4 votes vote down vote up
Wrapper: React.FC<Props> = ({
  style,
  children,
  layout,
  maxWidth,
  maxHeight,
  moduleId,
  itemAlign = 'center',
  visible = true
}) => {
  // Wrapper 自身的样式
  const [basicStyle, setBasicStyle] = useState<{ [keys: string]: any }>({});
  const actId = useSelector((state: RootState) => state.controller.editingId);
  const [wrapSize, setWrapSize] = useState<{ width: string; height: string }>();
  const setEditingId = useDispatch<Dispatch>().controller.setEditingId;
  const currentLayout = useSelector((state: RootState) => state.appData).filter(
    (item) => item.moduleId === actId,
  )?.[0]?.layout;

  const refWrap = useRef<HTMLDivElement>(null);
  const [ref, inView] = useInView({
    threshold: 0,
  });
  const isEditing = useSelector(
    (state: RootState) => state.controller.isEditing,
  );

  const sendMessage = usePostMessage(() => {});

  // base元素的样式控制
  const { basic } = style;
  const { display, animation } = basic || {};

  useEffect(() => {
    if (display?.zIndex !== undefined && refWrap.current?.parentElement) {
      refWrap.current.parentElement.style.zIndex = `${display.zIndex}`;
    }
  }, [moduleId, display?.zIndex]);

  // 样式编译
  useEffect(() => {
    setBasicStyle(styleCompiler({ ...basic }, inView));
  }, [moduleId, basic, inView]);

  // 仅针对有延时的入场动画在延时期间做元素隐藏处理,进入动画再做呈现
  const timer = useRef<NodeJS.Timeout | null>();
  useEffect(() => {
    if (!animation?.animationDelay) return;
    refWrap.current?.classList.add(s.hide);
    if (timer.current) window.clearTimeout(timer.current);
    timer.current = setTimeout(
      () => refWrap.current?.classList.remove(s.hide),
      animation.animationDelay + 50,
    );
  }, [basic, animation, inView]);

  // 计算尺寸
  useEffect(() => {
    if (refWrap.current) {
      setWrapSize({
        width: `${refWrap.current.offsetWidth}px`,
        height: `${refWrap.current.offsetHeight}px`,
      });
    }
  }, [refWrap, currentLayout?.w, currentLayout?.h]);

  /**
   * 图层被触发
   */
  const onLayoutClick = useCallback(() => {
    if (!isEditing) return;
    setEditingId(moduleId);
    // 向父级窗口通知当前激活Id
    sendMessage({ tag: 'id', value: moduleId }, window.top);
  }, [isEditing, moduleId, sendMessage, setEditingId]);
  /**设置预览状态下不接受编辑事件 */
  const pointerEvents: React.CSSProperties = {};
  if (isEditing) {
    pointerEvents.pointerEvents = 'none';
  } else {
    delete pointerEvents.pointerEvents;
  }
  /*设置最大尺寸*/
  const defaultSize: AnyObjectType = {};
  if (maxWidth && wrapSize?.width) {
    defaultSize.width = wrapSize?.width;
  }
  if (maxHeight && wrapSize?.height) {
    defaultSize.height = wrapSize?.height;
  }
  /*是否为隐藏模块*/
  const isHide = layout?.w === 0 || layout?.h === 0;
  if (isHide) {
    defaultSize.width = defaultSize.height = 'auto';
  }
  if (visible === false) return null;
  
  return (
    <div
      className={classNames(s.touchwrap, {
        [s.aligncenter]: itemAlign === 'center',
        [s.aligntop]: itemAlign === 'top',
        [s.alignbottom]: itemAlign === 'bottom',
      })}
      onTouchStart={onLayoutClick}
      onMouseDown={onLayoutClick}
      ref={refWrap}
    >
      {actId === moduleId ? (
        <div
          className={classNames(s.actwrap, {
            [s.isedit]: isEditing,
            [s.iswiew]: !isEditing,
          })}
        />
      ) : null}
      <div ref={ref} className={s.animationwrap} style={{ ...defaultSize }}>
        <div
          id={moduleId}
          className={s.secondwrap}
          style={{ ...defaultSize, ...basicStyle.style, ...pointerEvents }}
        >
          {children}
        </div>
      </div>
    </div>
  );
}
Example #11
Source File: LandingTidbits.tsx    From frontend.ro with MIT License 4 votes vote down vote up
LandingTidbits = ({ tidbits }: { tidbits: TidbitI[] }) => {
  const { ref, inView } = useInView({
    threshold: 0.4,
    triggerOnce: true,
  });

  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    if (!inView) {
      return noop;
    }

    const TIMEOUT_DURATION = 3000;
    const timeoutId = setTimeout(nextPage, TIMEOUT_DURATION);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [currentIndex, inView]);

  const nextPage = () => {
    if (currentIndex + 1 === tidbits.length) {
      setCurrentIndex(0);
    } else {
      setCurrentIndex(currentIndex + 1);
    }
  };

  const previousPage = () => {
    if (currentIndex === 0) {
      setCurrentIndex(tidbits.length - 1);
    } else {
      setCurrentIndex(currentIndex - 1);
    }
  };

  return (
    <section
      className={`
        ${styles.LandingTidbits}
        d-flex
        align-items-center
        justify-content-between
      `}
      style={{
        backgroundColor: tidbits[currentIndex].backgroundColor,
        color: tidbits[currentIndex].textColor,
      }}
      ref={ref}
    >
      <div className={`${styles.about} d-flex flex-column justify-content-between`}>
        <a
          rel="noreferrer"
          href={`/tidbits/${tidbits[currentIndex].tidbitId}/${currentIndex + 1}`}
          className={`${styles['heading-link']} no-underline`}
        >
          <h2 className={`${styles.heading} m-0`}>
            {tidbits[currentIndex].title}
          </h2>
        </a>
        <div className={styles.controls}>
          <div>
            <Button variant="transparent" onClick={previousPage}>
              <FontAwesomeIcon width={32} icon={faArrowAltCircleLeft} />
            </Button>
            <Button variant="transparent" onClick={nextPage}>
              <FontAwesomeIcon width={32} icon={faArrowAltCircleRight} />
            </Button>
          </div>
          {inView && (
            <Progress key={currentIndex} color={tidbits[currentIndex].textColor} />
          )}
        </div>
      </div>
      <StackedImages
        images={tidbits.map((t, index) => ({
          href: `/tidbits/${tidbits[currentIndex].tidbitId}/${currentIndex + 1}`,
          alt: t.title,
          src: t.items[1].imageSrc,
        }))}
        rotationDelta={15}
        currentIndex={currentIndex}
        className={styles.StackedImages}
      />
    </section>
  );
}
Example #12
Source File: index.tsx    From basement-grotesque with SIL Open Font License 1.1 4 votes vote down vote up
DemoSection = () => {
  const { fontsLoaded } = useAppContext()
  const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.05 })
  const [inputs, setInputs] = useState<Inputs>({
    size: {
      label: 'Size',
      value: '108',
      min: 21,
      max: 195,
      step: 1,
      renderValue: (value) => value + 'PX'
    },
    tracking: {
      label: 'Tracking',
      value: '-2',
      min: -4,
      max: 4,
      step: 0.1,
      renderValue: (value) => value + 'px'
    },
    leading: {
      label: 'Leading',
      value: '110',
      min: 83,
      max: 140,
      step: 1,
      renderValue: (value) => value + '%'
    }
  })
  const [text, setText] = useState('We Make Cool Shit That Performs')

  const handleChange: RangeProps['onChange'] = useCallback((e) => {
    const { name, value } = e.target
    const key = name as Name
    setInputs((p) => {
      return { ...p, [key]: { ...p[key], value } }
    })
  }, [])

  const handleTextChange: React.ChangeEventHandler<HTMLTextAreaElement> =
    useCallback((e) => {
      setText(e.target.value)
    }, [])

  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth < 900) {
        setInputs((p) => ({
          ...p,
          tracking: { ...p.tracking, value: '0.4' },
          size: { ...p.size, min: 14, max: 100, value: '32' }
        }))
      } else {
        setInputs((p) => ({
          ...p,
          size: { ...p.size, min: 21, max: 195, value: '108' }
        }))
      }
    }

    handleResize()
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  useEffect(() => {
    gsap.set('.demo__section', {
      autoAlpha: 0
    })
  }, [])

  useEffect(() => {
    if (!inView) return

    const items = document?.querySelector('.demo__section')?.childNodes

    if (!items) return

    const tl = gsap.timeline({
      paused: true,
      smoothChildTiming: true
    })
    tl.to('.demo__section', {
      autoAlpha: 1,
      duration: DURATION / 2
    })
    tl.from(items, {
      autoAlpha: 0,
      y: 30,
      stagger: 0.1
    })

    tl.play()

    return () => {
      tl.kill()
    }
  }, [inView])

  return (
    <Section
      background="black"
      css={{
        py: 88,
        '@bp2': {
          zIndex: 10,
          py: 128
        }
      }}
    >
      <Container withoutPx maxWidth ref={ref}>
        <Container className="demo__section">
          <SectionHeading
            title={<>Try this beauty&nbsp;:)</>}
            subtitle={
              <Box
                as="span"
                css={{ display: 'none', '@bp2': { display: 'inline' } }}
              >
                Handcrafted
                <br />
                for strong designs
              </Box>
            }
          />
          <Container
            css={{
              display: 'none',
              '@bp2': {
                display: 'block'
              }
            }}
            withoutPx
          >
            <PreviewContainer>
              <PreviewLabel>
                <p>
                  {Object.keys(inputs).map((key, i, { length }) => {
                    const isLast = i === length - 1
                    const input = inputs[key as Name]
                    return (
                      <Fragment key={i}>
                        {input.label[0]}: {input.renderValue(input.value)}
                        {isLast ? null : ' | '}
                      </Fragment>
                    )
                  })}
                </p>
                <ResizableTextarea
                  value={text}
                  className={textareaCss}
                  style={{
                    fontSize: inputs.size.value + 'px',
                    lineHeight: inputs.leading.value + '%',
                    letterSpacing: inputs.tracking.value + 'px',
                    fontFamily: 'var(--fonts-heading)'
                  }}
                  onChange={handleTextChange}
                  fontsLoaded={fontsLoaded}
                />
              </PreviewLabel>
            </PreviewContainer>
          </Container>
          <Box css={{ '@bp2': { display: 'none' } }}>
            <PreviewContainer>
              <PreviewLabel>
                <p>
                  {Object.keys(inputs).map((key, i, { length }) => {
                    const isLast = i === length - 1
                    const input = inputs[key as Name]
                    return (
                      <Fragment key={i}>
                        {input.label[0]}: {input.renderValue(input.value)}
                        {isLast ? null : ' | '}
                      </Fragment>
                    )
                  })}
                </p>
                <ResizableTextarea
                  value={text}
                  className={textareaCss}
                  style={{
                    fontSize: inputs.size.value + 'px',
                    lineHeight: inputs.leading.value + '%',
                    letterSpacing: inputs.tracking.value + 'px',
                    fontFamily: 'var(--fonts-heading)'
                  }}
                  onChange={handleTextChange}
                  fontsLoaded={fontsLoaded}
                />
              </PreviewLabel>
            </PreviewContainer>
          </Box>
          <InputsContainer>
            {Object.keys(inputs).map((key) => {
              return (
                <Range
                  {...inputs[key as Name]}
                  name={key}
                  key={key}
                  onChange={handleChange}
                />
              )
            })}
          </InputsContainer>
        </Container>
      </Container>
    </Section>
  )
}
Example #13
Source File: index.tsx    From basement-grotesque with SIL Open Font License 1.1 4 votes vote down vote up
CharactersSection = () => {
  const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })
  const [viewAll, setViewAll] = useState(false)
  const [gridHeight, setGridHeight] = useState<number>()
  const gridRef = useRef<HTMLDivElement>(null)

  const handleToggleViewAll = useCallback(() => setViewAll((p) => !p), [])

  const handleCopyGlyph: React.MouseEventHandler<HTMLButtonElement> =
    useCallback(async (e) => {
      const glyph = e.currentTarget.innerText
      try {
        await navigator.clipboard.writeText(glyph)
        toast.success(`Copied to clipboard`, {
          icon: glyph,
          style: {
            borderRadius: '10px',
            backgroundColor: 'black',
            color: 'white'
          }
        })
      } catch (error) {
        toast.error(`Failed to copy ${glyph} to clipboard`, {
          style: {
            borderRadius: '10px',
            backgroundColor: 'black',
            color: 'white'
          }
        })
      }
    }, [])

  useEffect(() => {
    const handleResize = () => {
      if (!gridRef.current) return
      setGridHeight(gridRef.current.offsetHeight)
    }

    handleResize()
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  useEffect(() => {
    gsap.set('#characters-section', {
      autoAlpha: 0
    })
  }, [])

  useEffect(() => {
    if (!inView) return

    const title = new SplitText('.characters__title', {
      type: 'lines,words,chars'
    })

    const tl = gsap.timeline({
      paused: true,
      smoothChildTiming: true
    })
    tl.to('#characters-section', {
      autoAlpha: 1,
      duration: DURATION / 2
    })
    tl.in(title.chars)
    tl.from(
      '.characters__svg',
      {
        autoAlpha: 0,
        y: 30
      },
      '< 80%'
    )

    tl.timeScale(1.5).play()

    return () => {
      tl.kill()
    }
  }, [inView])

  return (
    <Section background="black">
      <SectionInner
        id="characters-section"
        autoPy
        css={{ pb: viewAll ? '128px' : '0px' }}
        maxWidth
        ref={ref}
      >
        <Box
          css={{
            position: 'relative',
            py: '64px',
            mb: '64px',
            '@bp2': {
              py: '128px',
              mb: '128px'
            }
          }}
        >
          <Box
            css={{
              position: 'absolute',
              pointerEvents: 'none',
              left: '50%',
              top: '50%',
              transform: 'translate(-50%, -50%)'
            }}
          >
            <svg
              className="characters__svg"
              width="638"
              height="810"
              viewBox="0 0 638 810"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M528.721 68.2758C543.566 77.5248 551.979 95.6945 554.372 120.803C556.765 145.901 553.132 177.835 543.988 214.45C525.702 287.673 485.406 379.5 427.425 472.562C369.443 565.624 304.776 642.267 247.108 690.953C218.272 715.299 191.208 732.635 167.624 741.549C144.031 750.466 124.013 750.923 109.168 741.674C94.3235 732.425 85.9114 714.255 83.5175 689.147C81.1245 664.049 84.758 632.115 93.9019 595.5C112.188 522.277 152.484 430.45 210.465 337.388C268.447 244.326 333.114 167.683 390.782 118.997C419.618 94.6513 446.682 77.3151 470.266 68.4012C493.859 59.4839 513.877 59.0269 528.721 68.2758Z"
                stroke="white"
              />
            </svg>
          </Box>
          <Box
            as="p"
            className="characters__title"
            css={{
              fontFamily: '$heading',
              fontSize: '48px',
              fontWeight: 800,
              wordBreak: 'break-all',
              textAlign: 'center',
              lineHeight: '1',
              maxWidth: '1280px',
              margin: 'auto',

              '@bp2': {
                fontSize: '88px'
              }
            }}
            data-scroll-speed={-0.6}
            data-scroll
          >
            ABCDEFGHIJKLMNOPQRSTUVWXYZ
            <br />
            abcdefghijklmnopqrstuvwxyz
            <br />
            0123456789!?&
          </Box>
        </Box>
        <SectionHeading
          title="Characters"
          subtitle={
            <>
              413 glyphs
              <br />
              Black (800)
              <br />
              OTF
            </>
          }
        />
        <DesktopOnlyBox
          css={{
            transition: gridHeight ? `height ${gridHeight / 4000}s` : undefined,
            height: viewAll ? gridHeight : '570px',
            overflow: 'hidden'
          }}
        >
          <Box
            ref={gridRef}
            css={{
              display: 'grid',
              gridTemplateColumns: 'repeat(12, 1fr)',
              gridColumnGap: `min(18px, ${toVw(16)})`,
              gridRowGap: `min(18px, ${toVw(16)})`,
              overflow: 'hidden',
              pt: '80px',
              pb: '30px'
            }}
          >
            {glyphs.split('').map((glyph, i) => (
              <Glyph key={i} onClick={handleCopyGlyph}>
                {glyph}
              </Glyph>
            ))}
          </Box>
        </DesktopOnlyBox>
      </SectionInner>
      <DesktopOnlyBox>
        {!viewAll && (
          <Box
            as="button"
            onClick={handleToggleViewAll}
            css={{
              appearance: 'none',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              width: '100%',
              background: '$white',
              color: '$black',
              borderTop: '1px solid $black',
              py: '24px',
              textTransform: 'uppercase',
              '&:focus': {
                outline: 'none'
              },
              fontFamily: '$heading',
              svg: {
                ml: '8px',
                transition: 'all 250ms',
                fill: 'currentColor',
                color: '$black'
              }
            }}
          >
            {viewAll ? 'View Less' : 'View All'}{' '}
            <ArrowDown
              css={{
                color: 'black',
                path: { stroke: 'black' },
                $$size: '15px'
              }}
            />
          </Box>
        )}
      </DesktopOnlyBox>
      <MobileSection>
        <Marquee gradient={false} speed={50}>
          <Box
            css={{
              display: 'grid',
              gridTemplateRows: 'repeat(4, 1fr)',
              gridColumnGap: '16px',
              gridRowGap: '16px',
              overflow: 'hidden',
              gridAutoFlow: 'column',
              mx: '8px',
              py: '20px'
            }}
          >
            {mobileGlyphs.split('').map((glyph, i) => (
              <Glyph key={i} onClick={handleCopyGlyph}>
                {glyph}
              </Glyph>
            ))}
          </Box>
        </Marquee>
      </MobileSection>
    </Section>
  )
}
Example #14
Source File: index.tsx    From basement-grotesque with SIL Open Font License 1.1 4 votes vote down vote up
AboutSection = () => {
  const { ref, inView } = useInView({ triggerOnce: true, threshold: 0.2 })

  useEffect(() => {
    gsap.set('#about-section', {
      autoAlpha: 0
    })
  }, [])

  useEffect(() => {
    if (!inView) return

    const title = new SplitText('.about__title', {
      type: 'lines,words,chars'
    })

    const subtitle = new SplitText('.about__subtitle', {
      type: 'lines'
    })

    const tl = gsap.timeline({
      paused: true,
      smoothChildTiming: true
    })
    tl.to('#about-section', {
      autoAlpha: 1,
      duration: DURATION / 2
    })
    tl.in(title.lines)
    tl.in(subtitle.lines, '<40%')

    tl.timeScale(1.3).play()

    return () => {
      tl.kill()
    }
  }, [inView])

  return (
    <Section background="black" css={{ pt: '128px' }} id="about-section">
      <Container autoPy css={{ pb: 0 }} maxWidth ref={ref}>
        <Box
          css={{
            display: 'grid',
            gridTemplateColumns: '1fr',
            gap: '64px',
            '@bp2': {
              gridTemplateColumns: '1fr 1fr'
            }
          }}
        >
          <Box
            data-scroll-speed={0.6}
            data-scroll
            css={{
              maxWidth: 708,
              gridRowStart: 2,
              '@bp2': { gridRowStart: 'initial' }
            }}
          >
            <Text
              className="about__title"
              size="bg"
              css={{
                marginBottom: 48,
                '@bp2': { marginBottom: 28 }
              }}
            >
              <b>Basement Grotesque</b> is the studio’s first venture into the
              daunting but exciting world of type design. Of course, we had to
              start with a heavyweight: striking and unapologetically so; flawed
              but charming and full of character.
            </Text>
            <Text
              className="about__subtitle"
              css={{
                mb: 16
              }}
            >
              We set out inspired by the expressiveness of early 19th-century
              grotesque typefaces and the boldness and striking visuals of the
              contemporary revival of brutalist aesthetics. Grotesque is the
              first step in a very ambitious path we’ve set for ourselves.
            </Text>
            <Text className="about__subtitle">
              The typeface is a work in progress, open to anyone who shares our
              visual and graphic sensibilities. You're invited to check our
              journey as we iterate, change, and add new weights and widths in
              the future as we learn by doing.
            </Text>
          </Box>
          <Box
            css={{
              display: 'flex',
              alignItems: 'center',
              '@bp2': { margin: '-128px -128px -128px -32px' }
            }}
          >
            <AbAnimation />
          </Box>
        </Box>
        <Box
          data-scroll-speed={0.1}
          data-scroll
          css={{
            ta: 'center',
            fontWeight: 800,
            fontFamily: '$heading',
            fontSize: 'clamp($4, 2vw, $7)',
            lineHeight: 'clamp($5, 2.2vw, $8)',
            mt: '64px',
            '@bp2': {
              mt: '128px'
            }
          }}
        >
          ***
        </Box>
      </Container>
    </Section>
  )
}
Example #15
Source File: index.tsx    From basement-grotesque with SIL Open Font License 1.1 4 votes vote down vote up
Footer = () => {
  const { inView, ref } = useInView({ triggerOnce: true })

  return (
    <Section
      css={{
        paddingBottom: 40,
        paddingTop: 48
      }}
      background="black"
      data-scroll-section
      noMargin
    >
      <Container maxWidth>
        <FooterGrid>
          <Box
            css={{ position: 'relative', overflow: 'hidden' }}
            className="fallingLetters"
            ref={ref}
          >
            {inView && <FooterAnimation />}
          </Box>

          <Box
            className="social"
            css={{
              display: 'flex',
              flexDirection: 'column',
              justifyContent: 'space-between',
              padding: 20
            }}
          >
            <Box>
              <Text
                css={{
                  fontSize: 'min(60px, 5.4vw)',
                  lineHeight: 1,
                  '@bp2': { fontSize: 'min(60px, 2.8vw)' }
                }}
                uppercase
                heading
                tight
              >
                Our work is serious,{' '}
                <Text
                  css={{ lineHeight: 1.5, display: 'block' }}
                  as="span"
                  heading
                  outlined
                >
                  we are not.
                </Text>
              </Text>
            </Box>
            <Social css={{ marginTop: '$4' }}>
              {social.map(({ label, href }, idx) => (
                <li key={idx}>
                  <a href={href} target="_blank" rel="noopener">
                    <Text
                      className="label"
                      css={{
                        fontSize: '$3',
                        '@bp2': { fontSize: 'min(24px, 1.3vw)' }
                      }}
                      heading
                      uppercase
                    >
                      {label}
                    </Text>

                    <Box
                      css={{
                        width: 10,
                        height: 10,
                        '@bp2': { width: 19, height: 19 }
                      }}
                      className="arrow"
                    >
                      <ArrowUp
                        css={{
                          $$size: '$3',
                          '@bp2': { $$size: 'min(20px, 1.3vw)' }
                        }}
                      />
                    </Box>
                  </a>
                </li>
              ))}
            </Social>
          </Box>
          <Box
            className="policies"
            css={{
              display: 'flex',
              justifyContent: 'space-between',
              fontSize: '$1',
              '@bp2': { fontSize: '$3' }
            }}
          >
            <FooterLink
              href="https://github.com/basementstudio/basement-grotesque/blob/master/LICENSE.txt"
              target="_blank"
              rel="noopener"
              css={{
                borderRight: '1px solid $colors$white'
              }}
            >
              <Text className="label" as="span" uppercase heading>
                EULA
              </Text>
            </FooterLink>
            <FooterLink
              target="_blank"
              rel="noopener"
              href="mailto:[email protected]"
            >
              <Text className="label" as="span" uppercase heading>
                Contact
              </Text>
            </FooterLink>
          </Box>
          <Box
            className="legal"
            css={{
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center'
            }}
          >
            <Text css={{ fontSize: '$1', '@bp2': { fontSize: '$3' } }}>
              © basement.studio LLC 2021 all rights reserved
            </Text>
          </Box>
        </FooterGrid>
      </Container>
    </Section>
  )
}
Example #16
Source File: VideoView.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoView: React.FC = () => {
  useRedirectMigratedContent({ type: 'video' })
  const { id } = useParams()
  const { openNftPutOnSale, cancelNftSale, openNftAcceptBid, openNftChangePrice, openNftPurchase, openNftSettlement } =
    useNftActions()
  const { withdrawBid } = useNftTransactions()
  const { loading, video, error } = useVideo(id ?? '', {
    onError: (error) => SentryLogger.error('Failed to load video data', 'VideoView', error),
  })
  const nftWidgetProps = useNftWidget(id)

  const mdMatch = useMediaMatch('md')
  const { addVideoView } = useAddVideoView()
  const {
    watchedVideos,
    cinematicView,
    actions: { updateWatchedVideos },
  } = usePersonalDataStore((state) => state)
  const category = useCategoryMatch(video?.category?.id)

  const { anyOverlaysOpen } = useOverlayManager()
  const { ref: playerRef, inView: isPlayerInView } = useInView()
  const pausePlayNext = anyOverlaysOpen || !isPlayerInView

  const { url: mediaUrl, isLoadingAsset: isMediaLoading } = useAsset(video?.media)
  const { url: thumbnailUrl } = useAsset(video?.thumbnailPhoto)

  const videoMetaTags = useMemo(() => {
    if (!video || !thumbnailUrl) return {}
    return generateVideoMetaTags(video, thumbnailUrl)
  }, [video, thumbnailUrl])
  const headTags = useHeadTags(video?.title, videoMetaTags)

  const { startTimestamp, setStartTimestamp } = useVideoStartTimestamp(video?.duration)

  // Restore an interrupted video state
  useEffect(() => {
    if (startTimestamp != null || !video) {
      return
    }
    const currentVideo = watchedVideos.find((v) => v.id === video?.id)
    setStartTimestamp(currentVideo?.__typename === 'INTERRUPTED' ? currentVideo.timestamp : 0)
  }, [watchedVideos, startTimestamp, video, setStartTimestamp])

  const channelId = video?.channel?.id
  const channelName = video?.channel?.title
  const videoId = video?.id
  const categoryId = video?.category?.id

  useEffect(() => {
    if (!videoId || !channelId) {
      return
    }
    addVideoView({
      variables: {
        videoId,
        channelId,
        categoryId,
      },
    }).catch((error) => {
      SentryLogger.error('Failed to increase video views', 'VideoView', error)
    })
  }, [addVideoView, videoId, channelId, categoryId])

  // Save the video timestamp
  // disabling eslint for this line since debounce is an external fn and eslint can't figure out its args, so it will complain.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const handleTimeUpdate = useCallback(
    throttle((time) => {
      if (video?.id) {
        updateWatchedVideos('INTERRUPTED', video.id, time)
      }
    }, 5000),
    [video?.id]
  )

  const handleVideoEnd = useCallback(() => {
    if (video?.id) {
      handleTimeUpdate.cancel()
      updateWatchedVideos('COMPLETED', video?.id)
    }
  }, [video?.id, handleTimeUpdate, updateWatchedVideos])

  // use Media Session API to provide rich metadata to the browser
  useEffect(() => {
    const supported = 'mediaSession' in navigator
    if (!supported || !video) {
      return
    }

    const artwork: MediaImage[] = thumbnailUrl ? [{ src: thumbnailUrl, type: 'image/webp', sizes: '640x360' }] : []

    navigator.mediaSession.metadata = new MediaMetadata({
      title: video.title || '',
      artist: video.channel.title || '',
      album: '',
      artwork: artwork,
    })

    return () => {
      navigator.mediaSession.metadata = null
    }
  }, [thumbnailUrl, video])

  if (error) {
    return <ViewErrorFallback />
  }

  if (!loading && !video) {
    return (
      <NotFoundVideoContainer>
        <EmptyFallback
          title="Video not found"
          button={
            <Button variant="secondary" size="large" to={absoluteRoutes.viewer.index()}>
              Go back to home page
            </Button>
          }
        />
      </NotFoundVideoContainer>
    )
  }

  const isCinematic = cinematicView || !mdMatch
  const sideItems = (
    <GridItem colSpan={{ xxs: 12, md: 4 }}>
      {!!nftWidgetProps && (
        <NftWidget
          {...nftWidgetProps}
          onNftPutOnSale={() => id && openNftPutOnSale(id)}
          onNftCancelSale={() => id && nftWidgetProps.saleType && cancelNftSale(id, nftWidgetProps.saleType)}
          onNftAcceptBid={() => id && openNftAcceptBid(id)}
          onNftChangePrice={() => id && openNftChangePrice(id)}
          onNftPurchase={() => id && openNftPurchase(id)}
          onNftSettlement={() => id && openNftSettlement(id)}
          onNftBuyNow={() => id && openNftPurchase(id, { fixedPrice: true })}
          onWithdrawBid={() => id && withdrawBid(id)}
        />
      )}
      <MoreVideos channelId={channelId} channelName={channelName} videoId={id} type="channel" />
      <MoreVideos categoryId={category?.id} categoryName={category?.name} videoId={id} type="category" />
    </GridItem>
  )

  const detailsItems = (
    <>
      {headTags}
      <TitleContainer>
        {video ? (
          <TitleText variant={mdMatch ? 'h600' : 'h400'}>{video.title}</TitleText>
        ) : (
          <SkeletonLoader height={mdMatch ? 56 : 32} width={400} />
        )}
        <Meta variant={mdMatch ? 't300' : 't100'} secondary>
          {video ? (
            formatVideoViewsAndDate(video.views || null, video.createdAt, { fullViews: true })
          ) : (
            <SkeletonLoader height={24} width={200} />
          )}
        </Meta>
      </TitleContainer>
      <ChannelContainer>
        <ChannelLink followButton id={channelId} textVariant="h300" avatarSize="small" />
      </ChannelContainer>
      <VideoDetails video={video} category={category} />
    </>
  )

  return (
    <>
      <PlayerGridWrapper cinematicView={isCinematic}>
        <PlayerWrapper cinematicView={isCinematic}>
          <PlayerGridItem colSpan={{ xxs: 12, md: cinematicView ? 12 : 8 }}>
            <PlayerContainer className={transitions.names.slide} cinematicView={cinematicView}>
              {!isMediaLoading && video ? (
                <VideoPlayer
                  isVideoPending={!video?.media?.isAccepted}
                  channelId={video?.channel?.id}
                  videoId={video?.id}
                  autoplay
                  src={mediaUrl}
                  onEnd={handleVideoEnd}
                  onTimeUpdated={handleTimeUpdate}
                  startTime={startTimestamp}
                  isPlayNextDisabled={pausePlayNext}
                  ref={playerRef}
                />
              ) : (
                <PlayerSkeletonLoader />
              )}
            </PlayerContainer>
            {!isCinematic && detailsItems}
          </PlayerGridItem>
          {!isCinematic && sideItems}
        </PlayerWrapper>
      </PlayerGridWrapper>
      <LimitedWidthContainer>
        {isCinematic && (
          <LayoutGrid>
            <GridItem className={transitions.names.slide} colSpan={{ xxs: 12, md: cinematicView ? 8 : 12 }}>
              {detailsItems}
            </GridItem>
            {sideItems}
          </LayoutGrid>
        )}
        <StyledCallToActionWrapper>
          {['popular', 'new', 'discover'].map((item, idx) => (
            <CallToActionButton key={`cta-${idx}`} {...CTA_MAP[item]} />
          ))}
        </StyledCallToActionWrapper>
      </LimitedWidthContainer>
    </>
  )
}
Example #17
Source File: HtmlLanding.tsx    From frontend.ro with MIT License 4 votes vote down vote up
HtmlLanding = ({ isLoggedIn }: ConnectedProps<typeof connector>) => {
  const router = useRouter();
  const [showHtmlCssJs, setShowHtmlCssJs] = useState(false);
  const chipRows = [
    ['<html>', '<div>', '<form>', '<head>', '<span>', '<article>', '<video>', '<button>', '<title>', '<main>', '<label>', '<summary>'],
    ['<aside>', '<pre>', '<code>', '<em>', '<br>', '<body>', '<header>', '<section>', '<p>', '<nav>', '<tbody>', '<progress>', '<h1>'],
    ['<blockquote>', '<ol>', '<footer>', '<audio>', '<img>', '<picture>', '<h2>', '<canvas>', '<figure>', '<hr>', '<ul>', '<select>'],
    ['<a>', '<time>', '<h3>', '<track>', '<iframe>', '<svg>', '<script>', '<link>', '<table>', '<input>', '<textarea>', '<details>'],
  ];

  const { ref, inView } = useInView({
    threshold: 1,
    triggerOnce: true,
  });

  const startTutorial = () => {
    // Temporary redirect to the lessons page,
    // until we finish implementing the HTML Tutorial page.
    router.push('/lectii');
  };

  const navigateToFirstSection = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    document.querySelector('#what-is-html').scrollIntoView();
  };

  const navigateToLogin = (e: React.MouseEvent<HTMLAnchorElement>) => {
    e.preventDefault();
    document.querySelector('#auth').scrollIntoView();
  };

  useEffect(() => {
    if (inView) {
      setShowHtmlCssJs(true);
    }
  }, [inView]);

  withSmoothScroll();

  return (
    <>
      <Header theme="black" withNavMenu />
      <main data-certification-page className={styles.HtmlLanding}>
        <div className={`${styles.hero} d-flex align-items-center justify-content-center bg-black text-white`}>
          <h1 className="text-center mb-0"> Învață HTML de la zero </h1>
          <p>
            printr-un curs online, focusat pe
            {' '}
            <span className={styles['text-chip']}>
              practică și feedback
            </span>
            {' '}
            de la developeri cu experiență
          </p>
          <div className={`${styles['hero-controls']} d-flex align-items-center justify-content-between`}>
            <a onClick={navigateToFirstSection} href="#what-is-html" className="btn btn--light no-underline">
              Află mai multe
            </a>

            {isLoggedIn ? (
              <Button variant="blue" onClick={startTutorial}>
                Începe acum
              </Button>
            ) : (
              <a onClick={navigateToLogin} href="#auth" className="btn btn--blue no-underline">
                Începe acum
              </a>
            )}
          </div>
          <div className={styles['hero-video']} />
        </div>

        <div id="what-is-html">
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Ce este HTML-ul?
            </h2>

            <p className={styles['section-text']}>
              Este punctul de start în călătoria fiecărui
              {' '}
              <a href="/intro/ce-este-frontend-ul">FrontEnd developer</a>
              .
            </p>
            <p className={styles['section-text']}>
              Alături de
              {' '}
              <span className="text-bold"> CSS </span>
              {' '}
              și
              {' '}
              <span className="text-bold"> JavaScript </span>
              {' '}
              este unul din cele 3 limbaje pe care
              trebuie să le înveți pentru a construi site-uri și aplicații web.
            </p>
          </div>
          <div ref={ref} />
          <div className={styles['HtmlCssJs-wrapper']}>
            {showHtmlCssJs && <HtmlCssJs />}
          </div>
        </div>

        <div>
          <section className={styles.section}>
            <h2 className={styles['section-heading']}>
              Ce vei învăța?
            </h2>
            <p className={styles['section-text']}>
              Cursul acesta e destinat persoanelor cu zero (sau foarte puțină)
              experiență în FrontEnd, deci vom începe de la lucrurile de bază
              și apoi continua cu multă practică.
            </p>
            <List className={`${styles['learn-list']} ${styles['section-text']}`} variant="checkmark">
              <li className="mt-8">
                Ce e FrontEnd development-ul?
              </li>
              <li className="mt-4">
                Care e rolul HTML-ului în cadrul ramurii de FrontEnd?
              </li>
              <li className="mt-4">
                Cum să folosești
                {' '}
                <a href="https://code.visualstudio.com/" target="_blank" rel="noreferrer">
                  VSCode
                </a>
                {' '}
                ca și editor de cod
              </li>
              <li className="mt-4">
                Care sunt și cum să folosești cele mai importante elemente HTML
              </li>
              <li className="mt-4">
                Bune practici în zona de scriere a codului, accesibilitate și perfomanță
              </li>
            </List>

          </section>
          <ChipCarousel className="my-10" rows={chipRows} />
        </div>

        <div>
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Cum funcționează cursul?
            </h2>
          </div>
          <HtmlHowItWorks className={styles.HtmlHowItWorks} />
        </div>

        <div>
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Iar la final primești o certificare!
            </h2>
            <p className={styles['section-text']}>
              În programare nu contează prea mult diplomele în sine, ci ce știi să faci.
              De aceea, folosește această certificare ca o dovadă că ai reușit să scrii cod
              real, gata de producție, validat de developeri cu experiență!
            </p>
          </div>
          <div className={styles.section}>
            <HtmlFakeDiploma />
          </div>
        </div>

        <div id="auth">
          <div className={styles.section}>
            <h2 className={styles['section-heading']}>
              Gata să începem?
            </h2>
            {isLoggedIn && (
              <p className={styles['section-text']}>
                Ești deja autentificat, deci apasă pe butonul de mai jos și hai să trecem la treabă!
              </p>
            )}
          </div>
          {isLoggedIn ? (
            <div className="text-center text-2xl">
              <Button onClick={startTutorial} variant="blue">
                Începe acum
              </Button>
            </div>
          ) : (
            <div className={styles['login-wrapper']}>
              <Login
                mode="register"
                className={styles.login}
                onSuccess={startTutorial}
              />
            </div>
          )}
        </div>
      </main>
    </>
  );
}