repos

isaacbythewood.com-nextjs

mirror archived upstream

Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.

canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact

3.9 KB · 113 lines · JavaScript Raw History
  1import React, { useEffect, useState, useRef } from "react";
  2import { TransitionGroup, CSSTransition } from "react-transition-group";
  3import Image from "next/image";
  4
  5import Page from "../components/page";
  6import styles from "@styles/pages/index.module.css";
  7
  8const Index = () => {
  9  const words = ["AI Agents", "Automation", "DevOps", "Architecture"];
 10  const [currentWords, setCurrentWord] = useState([words[0]]);
 11  const [imageLoaded, setImageLoaded] = useState(false);
 12  const currentWordsRef = useRef(currentWords);
 13  currentWordsRef.current = currentWords;
 14  const wordRefs = useRef(new Map());
 15  const getWordRef = (key) => {
 16    if (!wordRefs.current.has(key)) {
 17      wordRefs.current.set(key, React.createRef());
 18    }
 19    return wordRefs.current.get(key);
 20  };
 21
 22  useEffect(() => {
 23    const wordsInterval = setInterval(() => {
 24      let nextWordIndex = words.indexOf(currentWordsRef.current[0]) + 1;
 25      if (nextWordIndex < words.length) setCurrentWord([words[nextWordIndex]]);
 26      else setCurrentWord([words[0]]);
 27    }, 2000);
 28    return () => {
 29      clearInterval(wordsInterval);
 30    };
 31  }, []);
 32
 33  const playState = imageLoaded ? "running" : "paused";
 34
 35  return (
 36    <Page title="Senior Solutions Architect at Craftmaster Furniture">
 37      <div className={styles.imageWrapper}>
 38        <Image
 39          src="/static/images/art/acrylic-pours/005.webp"
 40          alt="#005 Nebulas in Triangulum"
 41          loading="eager"
 42          priority={true}
 43          fill
 44          style={{ objectFit: "cover", objectPosition: "center" }}
 45          onLoad={() => {
 46            setImageLoaded(true);
 47            window.dispatchEvent(new Event("loaderReady"));
 48          }}
 49          onError={() => {
 50            // A failed hero load must still release the loader overlay, or
 51            // the page stays stuck behind it forever.
 52            setImageLoaded(true);
 53            window.dispatchEvent(new Event("loaderReady"));
 54          }}
 55        />
 56      </div>
 57      <TransitionGroup component="div" className={styles.words}>
 58        {currentWords.map((word) => {
 59          const nodeRef = getWordRef(word);
 60          return (
 61            <CSSTransition
 62              key={word}
 63              timeout={1000}
 64              classNames={{
 65                appear: styles.wordAppear,
 66                appearActive: styles.wordAppearActive,
 67                appearDone: styles.wordAppearDone,
 68                enter: styles.wordEnter,
 69                enterActive: styles.wordEnterActive,
 70                enterDone: styles.wordEnterDone,
 71                exit: styles.wordExit,
 72                exitActive: styles.wordExitActive,
 73              }}
 74              appear
 75              nodeRef={nodeRef}
 76            >
 77              <h3 className={styles.word} ref={nodeRef}>
 78                {word}
 79              </h3>
 80            </CSSTransition>
 81          );
 82        })}
 83      </TransitionGroup>
 84      <div className={styles.hero} style={{ animationPlayState: playState }}>
 85        <h1 className={styles.name}>
 86          <span className={styles.firstName}>Isaac</span>
 87          <span className={styles.lastName}>Bythewood</span>
 88        </h1>
 89        <div className={styles.role}>
 90          <span className={styles.roleTitle}>Senior Solutions Architect</span>
 91          <span className={styles.roleAt}>at</span>
 92          <span className={styles.roleCompany}>Craftmaster Furniture</span>
 93        </div>
 94        <a
 95          href="https://darkfurrow.com"
 96          target="_blank"
 97          rel="noopener noreferrer"
 98          className={styles.notice}
 99        >
100          <span className={styles.noticeLabel}>New Project</span>
101          <span className={styles.noticeTitle}>Dark Furrow</span>
102          <span className={styles.noticeDesc}>
103            An artistic almanac blending poetry, visual rhythm, and practical
104            wisdom for home gardeners and farmers.
105          </span>
106        </a>
107      </div>
108    </Page>
109  );
110};
111
112export default Index;