Animation-heavy personal portfolio on Next.js: a custom cursor, a page-transition loader, and canvas backgrounds.
canvas-animationscss-modulesdark-themehandcodednextjspersonal-websiteportfolioreact
1import React from "react";
2import PropTypes from "prop-types";
3import styles from "@styles/pages/code.module.css";
4
5import Page from "../components/page";
6
7const GitHubIcon = () => {
8 return (
9 <svg
10 xmlns="http://www.w3.org/2000/svg"
11 width="20"
12 height="20"
13 fill="currentColor"
14 viewBox="0 0 16 16"
15 >
16 <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
17 </svg>
18 );
19};
20
21const PROJECTS = [
22 {
23 name: "Taproot",
24 slug: "taproot",
25 description:
26 "Dotfiles, containers, and the configs that make a machine mine.",
27 tech: ["Docker", "Shell"],
28 },
29 {
30 name: "darkfurrow.com",
31 slug: "darkfurrow.com",
32 description:
33 "A living almanac of seasons, soil, and the quiet knowledge that used to be common.",
34 tech: ["Rust", "Axum"],
35 },
36 {
37 name: "Repos",
38 slug: "repos",
39 description:
40 "A minimal self-hosted git browser. Renders bare repos as a website with commits, diffs, syntax-highlighted blobs, and atom feeds, plus clone over HTTPS.",
41 tech: ["Rust", "Axum"],
42 },
43 {
44 name: "Analytics",
45 slug: "analytics",
46 description:
47 "A self-hostable analytics service with a straightforward API to track events from any source.",
48 tech: ["Rust", "Axum", "SQLite"],
49 },
50 {
51 name: "Status",
52 slug: "status",
53 description:
54 "A self-hosted uptime monitor and status page builder, with Lighthouse audits and PDF reports baked in.",
55 tech: ["Rust", "Axum", "SQLite"],
56 },
57 {
58 name: "Finance",
59 slug: "finance",
60 description:
61 "A self-hosted market watcher for stocks, ETFs, indexes, and futures, with live charts, key stats, fundamentals, and SEC filings.",
62 tech: ["Rust", "Axum", "SQLite"],
63 },
64 {
65 name: "blog.bythewood.me",
66 slug: "blog.bythewood.me",
67 description:
68 "A self-hostable markdown blog for developers, with code blocks, syntax highlighting, live search, great SEO, and a clean customizable UI.",
69 tech: ["Rust", "Axum"],
70 },
71 {
72 name: "Timelite",
73 slug: "timelite",
74 description:
75 "A simple time tracker that keeps everything local in your browser. No accounts, no sync, no server-side state.",
76 tech: ["Next.js", "JavaScript"],
77 },
78 {
79 name: "isaacbythewood.com",
80 slug: "isaacbythewood.com",
81 description: "The personal website you are looking at right now.",
82 tech: ["Next.js", "JavaScript"],
83 },
84];
85
86const Code = ({ commits }) => {
87 return (
88 <Page title="Code" description="Some of my most recent coding projects.">
89 <div className={styles.background} />
90 <h1 className={styles.heading}>Code</h1>
91 <p className={styles.paragraph}>
92 Outside of work I build self-hosted tools, personal websites, and
93 whatever else catches my interest. Side projects are where I experiment
94 with new technology and stay sharp since at work I stick to stable,
95 mature frameworks. There is plenty more to see on{" "}
96 <a
97 href="https://github.com/overshard"
98 rel="noopener noreferrer"
99 target="_blank"
100 >
101 my GitHub account
102 </a>
103 .
104 </p>
105 <div className={styles.projects}>
106 {PROJECTS.map((project, index) => (
107 <div
108 key={project.slug}
109 className={styles.project}
110 style={{ animationDelay: `${index * 100}ms` }}
111 >
112 <h2 className={styles.projectHeading}>{project.name}</h2>
113 <div className={styles.tags}>
114 {project.tech.map((tag) => (
115 <span key={tag} className={styles.tag}>
116 {tag}
117 </span>
118 ))}
119 </div>
120 <p className={styles.projectParagraph}>{project.description}</p>
121 {commits[project.slug] && (
122 <pre className={styles.projectCommit}>
123 {JSON.stringify(commits[project.slug], null, 2)}
124 </pre>
125 )}
126 <a
127 className={styles.projectButton}
128 href={`https://github.com/overshard/${project.slug}`}
129 rel="noopener noreferrer"
130 target="_blank"
131 >
132 <GitHubIcon />
133 GitHub
134 </a>
135 </div>
136 ))}
137 </div>
138 </Page>
139 );
140};
141
142export async function getStaticProps() {
143 const getCommit = async (repo) => {
144 try {
145 const res = await fetch(
146 `https://api.github.com/repos/overshard/${repo}/commits?per_page=1`
147 );
148 if (!res.ok) {
149 console.error(`GitHub API failed for ${repo}: ${res.status}`);
150 return null;
151 }
152 const commits = await res.json();
153 if (!Array.isArray(commits) || commits.length === 0) return null;
154 const commit = commits[0];
155 return {
156 sha: commit.sha ? commit.sha.substring(0, 7) : null,
157 message: commit.commit?.message || null,
158 date: commit.commit?.author?.date || null,
159 author: commit.commit?.author?.name || null,
160 };
161 } catch (err) {
162 console.error(`Failed to fetch commits for ${repo}:`, err);
163 return null;
164 }
165 };
166
167 const results = await Promise.all(
168 PROJECTS.map(async (project) => [
169 project.slug,
170 await getCommit(project.slug),
171 ])
172 );
173
174 const commits = Object.fromEntries(results);
175
176 return {
177 props: { commits },
178 revalidate: 3600,
179 };
180}
181
182Code.propTypes = {
183 commits: PropTypes.object,
184};
185
186export default Code;