Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.
canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact
1import React, { useState, useRef } from "react";
2
3import { CSSTransition } from "react-transition-group";
4import Image from "next/image";
5import Link from "next/link";
6import styles from "@styles/components/menu.module.css";
7
8const Menu = () => {
9 const [open, setOpen] = useState(false);
10 const overlayRef = useRef(null);
11 const gridLeftRef = useRef(null);
12 const gridRightRef = useRef(null);
13 const pages = [
14 { num: "000", href: "/", title: "Home" },
15 { num: "001", href: "/about", title: "About" },
16 { num: "002", href: "/code", title: "Code" },
17 { num: "003", href: "/art", title: "Art" },
18 { num: "004", href: "/contact", title: "Contact" },
19 ];
20
21 const toggleMenu = () => {
22 if (open === false) {
23 setOpen(true);
24 document.body.style.overflowY = "hidden";
25 } else {
26 setOpen(false);
27 document.body.style.overflowY = "scroll";
28 }
29 };
30
31 return (
32 <>
33 <button
34 className={styles.hamburger}
35 onClick={toggleMenu}
36 aria-label="Hamburger"
37 >
38 <div className={styles.patty} style={{ width: "15px" }} />
39 <div className={styles.patty} />
40 <div className={styles.patty} style={{ width: "20px" }} />
41 </button>
42 <CSSTransition
43 in={open}
44 timeout={500}
45 classNames="menu"
46 appear
47 nodeRef={overlayRef}
48 onEnter={() => {
49 if (typeof document !== "undefined") {
50 document.body.style.overflowY = "hidden";
51 }
52 }}
53 onExited={() => {
54 if (typeof document !== "undefined") {
55 document.body.style.overflowY = "scroll";
56 }
57 }}
58 >
59 <div className={styles.overlay} ref={overlayRef}>
60 <div className={styles.overlayGrid}>
61 <CSSTransition
62 in={open}
63 timeout={500}
64 classNames="menu"
65 appear
66 nodeRef={gridLeftRef}
67 >
68 <div className={styles.overlayGridLeft} ref={gridLeftRef}>
69 <div className={styles.topBar}>
70 <a
71 className={styles.topLink}
72 href="https://blog.bythewood.me/"
73 target="_blank"
74 rel="noopener noreferrer"
75 >
76 Blog
77 </a>
78 <a
79 className={styles.topLink}
80 href="https://status.bythewood.me/a40ff31d-18b0-49c3-9a36-deb02c909204"
81 target="_blank"
82 rel="noopener noreferrer"
83 >
84 Status
85 </a>
86 <a
87 className={styles.topLink}
88 href="https://analytics.bythewood.me/30e69c06-9beb-4283-8919-8c7a686ab013"
89 target="_blank"
90 rel="noopener noreferrer"
91 >
92 Analytics
93 </a>
94 <a
95 className={styles.topLink}
96 href="https://github.com/overshard"
97 target="_blank"
98 rel="noopener noreferrer"
99 >
100 GitHub
101 </a>
102 <div className={styles.bar} />
103 </div>
104 {pages.map((page) => {
105 return (
106 <Link
107 key={page.href}
108 href={page.href}
109 className={styles.overlayLink}
110 data-text={page.title}
111 onClick={toggleMenu}
112 >
113 {page.title}
114 </Link>
115 );
116 })}
117 <div className={styles.bottomBar}>
118 <Image
119 src="/static/images/avatar.webp"
120 alt="Isaac Bythewood"
121 width={50}
122 height={50}
123 />
124 <div className={styles.bar} />
125 </div>
126 </div>
127 </CSSTransition>
128 <CSSTransition
129 in={open}
130 timeout={500}
131 classNames="menu"
132 appear
133 nodeRef={gridRightRef}
134 >
135 <div className={styles.overlayGridRight} ref={gridRightRef}>
136 <Image
137 src="/static/images/art/acrylic-pours/006.webp"
138 alt="#006 Molten Copper"
139 loading="lazy"
140 fill
141 sizes="40vw"
142 style={{ objectFit: "cover", objectPosition: "center" }}
143 />
144 </div>
145 </CSSTransition>
146 </div>
147 </div>
148 </CSSTransition>
149 </>
150 );
151};
152
153export default Menu;