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.8 KB · 187 lines · JavaScript Raw History
  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;