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

5.5 KB · 145 lines · JavaScript Raw History
  1import React, { useState, useEffect, useRef } from "react";
  2import { TransitionGroup, CSSTransition } from "react-transition-group";
  3
  4import Page from "../components/page";
  5import styles from "@styles/pages/about.module.css";
  6
  7const INITIAL_WORDS = [
  8  "AI Agents",
  9  "DevOps",
 10  "Full-Stack",
 11  "Testing",
 12  "Cloud",
 13  "Security",
 14  "Developer",
 15  "Automation",
 16  "Architecture",
 17];
 18
 19const About = () => {
 20  const [words, setWords] = useState(INITIAL_WORDS);
 21  const wordsRef = useRef(words);
 22  wordsRef.current = words;
 23  const slots = useRef(
 24    Object.fromEntries(INITIAL_WORDS.map((word, i) => [word, i]))
 25  );
 26  const wordRefs = useRef(new Map());
 27  const getWordRef = (key) => {
 28    if (!wordRefs.current.has(key)) {
 29      wordRefs.current.set(key, React.createRef());
 30    }
 31    return wordRefs.current.get(key);
 32  };
 33
 34  useEffect(() => {
 35    const getRandomWordIndex = () => {
 36      return Math.floor(Math.random() * wordsRef.current.length);
 37    };
 38
 39    const wordsInterval = setInterval(() => {
 40      let firstWordIndex = getRandomWordIndex();
 41      let secondWordIndex = getRandomWordIndex();
 42      while (firstWordIndex === secondWordIndex) {
 43        firstWordIndex = getRandomWordIndex();
 44        secondWordIndex = getRandomWordIndex();
 45      }
 46      const firstWord = wordsRef.current[firstWordIndex];
 47      const secondWord = wordsRef.current[secondWordIndex];
 48      const firstSlot = slots.current[firstWord];
 49      const secondSlot = slots.current[secondWord];
 50      slots.current[firstWord] = secondSlot;
 51      slots.current[secondWord] = firstSlot;
 52      setWords(
 53        wordsRef.current.filter((word, index) => {
 54          return index !== firstWordIndex && index !== secondWordIndex;
 55        })
 56      );
 57      setTimeout(() => {
 58        let newWords = [...wordsRef.current];
 59        newWords.splice(secondWordIndex, 0, firstWord);
 60        newWords.splice(firstWordIndex, 0, secondWord);
 61        setWords(newWords);
 62      }, 1000);
 63    }, 4000);
 64
 65    return () => {
 66      clearInterval(wordsInterval);
 67    };
 68  }, []);
 69
 70  return (
 71    <Page title="About" description="A brief professional history of myself.">
 72      <div className={styles.background} />
 73      <div className={styles.words}>
 74        <TransitionGroup component={null}>
 75          {words.map((word) => {
 76            const nodeRef = getWordRef(word);
 77            return (
 78              <CSSTransition
 79                key={word}
 80                timeout={1000}
 81                appear
 82                classNames={{
 83                  appearActive: styles.wordAppearActive,
 84                  appearDone: styles.wordAppearDone,
 85                  enterActive: styles.wordEnterActive,
 86                  enterDone: styles.wordEnterDone,
 87                  exitActive: styles.wordExitActive,
 88                }}
 89                nodeRef={nodeRef}
 90              >
 91                <h2
 92                  className={styles.word}
 93                  ref={nodeRef}
 94                  style={{ top: `${slots.current[word] * 11}vh` }}
 95                >
 96                  <span className={styles.wordText}>{word}</span>
 97                </h2>
 98              </CSSTransition>
 99            );
100          })}
101        </TransitionGroup>
102      </div>
103      <p className={styles.paragraph}>
104        I am a{" "}
105        <span className={styles.strong}>senior solutions architect</span>{" "}
106        at Craftmaster Furniture with{" "}
107        <span className={styles.strong}>two decades in tech</span>.{" "}
108        I got my start{" "}
109        <span className={styles.strong}>modifying kernel modules</span>{" "}
110        for Digium Telephony Cards on CentOS and have been building across the{" "}
111        <span className={styles.strong}>full stack</span>{" "}
112        ever since, from DevOps and server infrastructure to{" "}
113        <span className={styles.strong}>highly regulated</span>{" "}
114        healthcare environments, leading teams through tight deadlines and
115        demanding delivery schedules. Today I focus on{" "}
116        <span className={styles.strong}>AI agent workflows</span>,{" "}
117        automated testing infrastructure, and tooling that keeps release cycles
118        fast without sacrificing stability or security.
119        <a
120          className={styles.resume}
121          href="/static/pdfs/resume-isaac-bythewood.pdf"
122          target="_blank"
123          rel="noopener noreferrer"
124        >
125          <svg
126            xmlns="http://www.w3.org/2000/svg"
127            width="20"
128            height="20"
129            fill="currentColor"
130            viewBox="0 0 16 16"
131          >
132            <path
133              fillRule="evenodd"
134              d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM1.6 11.85H0v3.999h.791v-1.342h.803c.287 0 .531-.057.732-.173.203-.117.358-.275.463-.474a1.42 1.42 0 0 0 .161-.677c0-.25-.053-.476-.158-.677a1.176 1.176 0 0 0-.46-.477c-.2-.12-.443-.179-.732-.179Zm.545 1.333a.795.795 0 0 1-.085.38.574.574 0 0 1-.238.241.794.794 0 0 1-.375.082H.788V12.48h.66c.218 0 .389.06.512.181.123.122.185.296.185.522Zm1.217-1.333v3.999h1.46c.401 0 .734-.08.998-.237a1.45 1.45 0 0 0 .595-.689c.13-.3.196-.662.196-1.084 0-.42-.065-.778-.196-1.075a1.426 1.426 0 0 0-.589-.68c-.264-.156-.599-.234-1.005-.234H3.362Zm.791.645h.563c.248 0 .45.05.609.152a.89.89 0 0 1 .354.454c.079.201.118.452.118.753a2.3 2.3 0 0 1-.068.592 1.14 1.14 0 0 1-.196.422.8.8 0 0 1-.334.252 1.298 1.298 0 0 1-.483.082h-.563v-2.707Zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638H7.896Z"
135            />
136          </svg>
137          My Resume
138        </a>
139      </p>
140    </Page>
141  );
142};
143
144export default About;