Compare commits

..

No commits in common. "main" and "add-decap" have entirely different histories.

72 changed files with 2707 additions and 19280 deletions

View file

@ -1,7 +0,0 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

View file

@ -1,2 +0,0 @@
# NEXT_PUBLIC_CONTENT_API_BASE_URL=http://localhost:8055
NEXT_PUBLIC_CONTENT_API_BASE_URL=https://cms.aaronjy.me

View file

@ -1,3 +1,6 @@
{ {
"extends": ["next/core-web-vitals"] "extends": "next/core-web-vitals",
"rules": {
"@stylistic/jsx/jsx-pascal-case": "off"
}
} }

8
.gitignore vendored
View file

@ -34,11 +34,3 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
deploy-vars.sh
node_modules
.env
.env.production
tmp/*
!.gitkeep

View file

@ -1 +0,0 @@
npx --no -- commitlint --edit $1

View file

@ -1 +1,3 @@
npx lint-staged echo Formatting...
npm run format
echo Successfully formatted.

1
.nvmrc
View file

@ -1 +0,0 @@
v22.15.0

47
.vscode/settings.json vendored
View file

@ -1,47 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"aaronjy",
"apos",
"Aurelia",
"Datatrial",
"Dirents", "Dirents",
"doesn", "Sitecore"
"fiftyfiveandfive", ]
"Firestore", }
"Goodreads",
"helpdesk",
"Hetzner",
"Integra",
"Levantine",
"opengraph",
"Orangebus",
"pipdig",
"pubdate",
"quot",
"Radr",
"Recon",
"Sitecore",
"sportank",
"Umbraco",
"Yarborough",
"Yarbz"
],
"files.autoSave": "off",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
}

View file

@ -1,71 +0,0 @@
FROM node:22-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED=1s
ARG NEXT_PUBLIC_CONTENT_API_BASE_URL
ENV NEXT_PUBLIC_CONTENT_API_BASE_URL=$NEXT_PUBLIC_CONTENT_API_BASE_URL
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View file

@ -1,2 +0,0 @@
// eslint-disable-next-line import/no-anonymous-default-export
export default { extends: ["@commitlint/config-conventional"] };

14
content/home.md Normal file
View file

@ -0,0 +1,14 @@
---
title: Awesome kitties
date: 2019-03-17T19:31:20.591Z
cats:
- description: 'Maru is a Scottish Fold from Japan, and he loves boxes.'
name: Maru (まる)
- description: Lil Bub is an American celebrity cat known for her unique appearance.
name: Lil Bub
- description: 'Grumpy cat is an American celebrity cat known for her grumpy appearance.'
name: Grumpy cat (Tardar Sauce)
---
Welcome to my awesome page about cats of the internet.
This page is built with NextJS, and content is managed in Decap CMS

163
content/pages/cv.yml Normal file
View file

@ -0,0 +1,163 @@
competencies:
- Software Development
- Software Architecture
- UI/UX Design
- Full-Stack Development
- Team Leadership
- Recruitment and Onboarding
- Web Application Development
- Cloud Hosting (AWS
- GCP)
- Database Management
- User Interface Design
- Project Management
education:
- 10 GCSEs @ Duchess Community High School
certifications:
- Sitecore Professional Developer Certification (Sitecore 8.2) - Aug. 2017
languages:
- name: English
proficiency: Native
- name: German
proficiency: Professional Working Proficiency
- name: Arabic (Levantine)
proficiency: Elementary
experience:
- position: Software Development Tutor
employer: Yarbz Digital Ltd
start: Sep. 2023
end: Mar. 2024
desc: I teach students of all levels modern software development, including
coding fundamentals, computer science theory and modern software
technologies. I also help them prepare for tech interviews!
- position: Freelance Software Consultant
employer: Yarbz Digital Ltd
start: Aug. 2021
end: Mar. 2024
desc: >-
* Designed and developed the front-end and back-end of the innovative
recruitment platform "Radr" using Angular 13, Node.js, TypeScript,
MongoDB, and hosted on Google Cloud Platform.
* Developed Fifty Five and Five's flagship website (fiftyfiveandfive.com) using PHP, WordPress, and Tailwind, enhancing its online presence.
* Built front-end of sportank.com, a dynamic social network catering to American Football enthusiasts and athletes, using Angular 10, Tailwind, SCSS, and TypeScript.
* Designed and built a proof-of-concept using Angular, Node.js, and gRPC, enabling real-time voice input streaming from web browsers to medical speech recognition software, leading to its active use by doctors for efficient communication.
* Maintained and improved the Integra Planner event management platform used by thousands of people for multi-day events, specifically for GARA Choruses.
* Enabled efficient event planning and management by utilizing Angular, Ionic for mobile apps, AWS, SQL Server, and .NET Core.
**Skills:** ASP.NET · Amazon Elastic Container Registry (ECR) · Angular · TypeScript · Amazon ECS · Python (Programming Language) · Node.js · ASP.NET MVC · Content Management Systems (CMS) · Amazon Web Services (AWS) · Next.js · Microsoft Azure · Git · React.js · C# · Full-Stack Development · Umbraco · WordPress · Web Development · JavaScript · Front-End Development · MySQL · Microsoft SQL Server · Cloud Development · PHP · NoSQL · AWS Lambda · HTML · Microservices · Technical Requirements · Firebase · ASP.NET Core · Agile Methodologies · Google Cloud Platform (GCP) · MongoDB · User Interface Programming
- position: Contract Software Engineer
employer: The Data Shed
start: Jan. 2023
end: Aug. 2023
desc: >-
* Facilitated the recovery of funds for more than 100,000 customers
affected by loan mis-selling by developing a robust web application using
Next.js, TypeScript, React, Node.js, AWS Cognito, and AWS ECS.
* Implemented a wide range of features, including voting, bank details collection, messaging functionality, claims processing, and document management.
**Skills:** Amazon Elastic Container Registry (ECR) · TypeScript · Amazon ECS · Tailwind CSS · Node.js · Amazon Web Services (AWS) · Next.js · React.js · docker · Front-End Development · NoSQL · AWS Lambda · HTML · Agile Methodologies · User Interface Programming
- position: Software Architecht
employer: T101
start: Sep. 2020
end: Jul. 2021
desc: >-
* As well as fulfilling the engineering responsibilities required by my
previous role, my responsibilities now additionally entail designing,
documenting and leading on architectural changes.As well as fulfilling the
engineering responsibilities required by my previous role, my
responsibilities now additionally entail designing, documenting and
leading on architectural changes.
**Skills:** ASP.NET · Angular · TypeScript · Amazon ECS · ASP.NET MVC · Kubernetes · Amazon Web Services (AWS) · Git · C# · Full-Stack Development · Web Development · JavaScript · Front-End Development · MySQL · Microsoft SQL Server · Cloud Development · NoSQL · HTML · .NET Core · Microservices · Technical Requirements · ASP.NET Core · Agile Methodologies · Google Cloud Platform (GCP) · User Interface Programming
- position: Senior Full-stack Developer
employer: T101
start: Feb. 2020
end: Sep. 2020
desc: >-
* Drove the complete platform re-architecture and development for Recon, a
UK-based dating app with 200,000 monthly active users.
* Modernized and scaled the app by using .NET Core, Firestore, SignalR, Angular 10/TypeScript, gRPC, SQL Server, MySQL, microservices and Kubernetes on GCP.
**Skills:** ASP.NET · Angular · TypeScript · Amazon ECS · ASP.NET MVC · Kubernetes · Git · C# · Full-Stack Development · Web Development · JavaScript · Front-End Development · MySQL · Microsoft SQL Server · Cloud Development · NoSQL · HTML · .NET Core · Microservices · Technical Requirements · Agile Methodologies · User Interface Programming
- position: Senior Software Developer
employer: Datatrial
start: Apr. 2019
end: Feb. 2020
desc: >-
* Worked on developing new/improving existing functional modules for
Datatrial's Nucleus offering, which aims to provide a web platform for
facilitating clinical trials.
**Skills:** ASP.NET · TypeScript · ASP.NET MVC · Git · C# · Full-Stack Development · Web Development · JavaScript · Front-End Development · MySQL · Microsoft SQL Server · Aurelia · HTML · Agile Methodologies · User Interface Programming
- position: Software Engineer
employer: pipdig
start: Aug 2018
end: Apr 2019
desc: >-
* Developing a mix of commercial sites and bespoke blogs, I was
responsible for the entire product life-cycle. This included requirements
gathering, development, management of the project and ultimately
delivering and maintaining the product. Responsibilities also included
improving internal software development practices and working to increase
efficiency across a wide range of small, fast-paced projects.
**Skills:** TypeScript · Content Management Systems (CMS) · Git · Full-Stack Development · WordPress · Web Development · JavaScript · Front-End Development · MySQL · Microsoft SQL Server · Cloud Development · PHP · HTML · Agile Methodologies · User Interface Programming
- position: Senior Software Developer
employer: The Works
start: Apr. 2018
end: Aug. 2018
desc: >-
* I was the sole developer for an event management platform at Newcastle
University, enhancing it by developing key features.
**Skills:** ASP.NET · ASP.NET MVC · Content Management Systems (CMS) · Git · C# · Full-Stack Development · Web Development · JavaScript · Front-End Development · Microsoft SQL Server · HTML · Agile Methodologies · User Interface Programming · Umbraco
- position: Software Developer
employer: Orangebus
start: Jan. 2017
end: Apr. 2018
desc: >-
* My responsibilities involved developing and managing a variety of
different projects across different industries.
**Skills:** ASP.NET MVC · Git · C# · Full-Stack Development · JavaScript · Front-End Development · Microsoft SQL Server · HTML · Agile Methodologies · User Interface Programming
- position: Junior Software Developer
employer: True Potential LLP
start: Oct. 2015
end: Dec. 2016
desc: >-
* Primarily a web developer, my responsibilities included developing
interactivity on the front-end, back-end services and designing database
structures for large-scale web applications that are in-use by over 2
million clients as of November 2016.
**Skills:** ASP.NET MVC · Git · C# · Full-Stack Development · Web Development · JavaScript · Visual Basic .NET (VB.NET) · Front-End Development · Microsoft SQL Server · HTML · Agile Methodologies · User Interface Programming
- position: IT Support / Trainee Software Developer
employer: Innovation Property (UK)
start: Jan. 2013
end: Sep. 2015
desc: >-
* I worked as an IT Support Technician and Developer Trainee. My
responsibilities included dealing with IT issues via an IT helpdesk
system. I also worked on improvements to internally-developed software
that was used by our Arboricultural staff. I also provided updates to an
internal MVC application used by office staff to log data, arrange
appointments for external staff and contact clients.
**Skills:** ASP.NET MVC · Git · C# · Full-Stack Development · JavaScript · Front-End Development · Microsoft SQL Server · HTML · Agile Methodologies · User Interface Programming

View file

@ -0,0 +1,16 @@
---
title: Manakish (pan)
description: Yummy yummy!
you-will-need: |-
* flower
* 1tbsp sugar
* 1 tsp yeast
* 1/4 cup vegetable oil
* 1/2 tsp baking powder
* cheese (mozarella, akkawi) or za'atar for filling
---
1. Mix the flower, sugar and yeast together in a bowl, and add a 1 cup of warm water. Keep mixing until it turns to a goo-like consistency
2. Cover the bowl for 10 minutes
3. Add the oil, baking powder and a further 1.5 cup of flower, and stir
4. Once the dough comes together, knead it until it is combined fully, and separate it into small blobs of dough.
5. Roll each blob out and fry it for 2 minutes on either side in the pan, adding your preferred filling on top while it's cooking (cheese or za'atar, or both)

View file

@ -1,10 +0,0 @@
source ./deploy-vars.sh
echo "Host: $SSH_HOST"
echo "Path: $SSH_UPLOAD_PATH"
echo "Deploying..."
rsync -r --delete ./out/ root@$SSH_HOST:$SSH_UPLOAD_PATH
echo "Done!"

View file

@ -1,17 +0,0 @@
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
// Add any custom config to be passed to Jest
const config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

View file

@ -2,8 +2,6 @@
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, }
"checkJs": true,
"jsx": "preserve"
} }
} }

View file

@ -1,51 +0,0 @@
const fs = require("fs");
const fm = require("front-matter");
const siteUrl = process.env.SITE_URL || "https://www.aaronjy.me";
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl,
changefreq: "weekly",
generateRobotsTxt: true,
autoLastmod: false,
generateIndexSitemap: false,
exclude: ["/server-sitemap-index.xml"], // <= exclude here
robotsTxtOptions: {
policies: [
{
userAgent: "*",
allow: "/",
},
],
additionalSitemaps: [
`${siteUrl}/server-sitemap-index.xml`, // <==== Add here
],
},
transform: async (config, path) => {
const metadata = {
loc: path,
};
if (isHomepage(path)) {
metadata.priority = 1;
} else if (isBasePage(path)) {
metadata.priority = 0.8;
} else if (isArticle(path)) {
metadata.priority = 0.6;
}
return metadata;
},
};
function isHomepage(path) {
return path === "/";
}
function isBasePage(path) {
return path.split("/").length === 2;
}
function isArticle(path) {
return path.startsWith("/writing/") || path.startsWith("/library/");
}

View file

@ -1,10 +1,6 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true
output: "standalone", }
images: {
unoptimized: true,
},
};
export default nextConfig; export default nextConfig

19510
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,66 +1,31 @@
{ {
"name": "www-aaronjy-me", "name": "www-aaronjy-2024",
"version": "2.4.1", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"server": "npx decap-server",
"build": "next build", "build": "next build",
"postbuild": "next-sitemap --config next-sitemap.config.cjs",
"start": "next start", "start": "next start",
"link": "echo NOT CONFIGURED", "link": "npx standard",
"format": "prettier . --write", "format": "npx standard --fix",
"prepare": "husky", "prepare": "husky"
"test": "jest --verbose --passWithNoTests",
"lint": "next lint",
"export:books": "node ./util/books-as-json.js > ./tmp/books.json",
"export:writing": "node ./util/writing-as-json.js > ./tmp/writing.json"
}, },
"dependencies": { "dependencies": {
"@highlightjs/cdn-assets": "^11.11.1", "next": "14.1.1",
"@mdx-js/mdx": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"camelcase-keys": "^9.1.3",
"date-fns": "^4.1.0",
"highlight.js": "^11.11.0",
"i": "^0.3.7",
"next": "^15.3.1",
"next-mdx-remote-client": "^1.1.0",
"next-seo": "^6.5.0", "next-seo": "^6.5.0",
"node-html-parser": "^7.0.1",
"npm": "^11.3.0",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18"
"rehype-code-titles": "^1.2.0",
"rehype-prism-plus": "^2.0.1",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.0", "@babel/core": "^7.24.0",
"@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.23.3",
"@babel/preset-react": "^7.24.7", "eslint": "^8",
"@commitlint/cli": "^19.1.0", "eslint-config-next": "14.1.1",
"@commitlint/config-conventional": "^19.1.0",
"@next/eslint-plugin-next": "^15.3.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.12",
"babel-jest": "^29.7.0",
"eslint": "8.57.1",
"eslint-config-next": "15.3.1",
"front-matter": "^4.0.2",
"frontmatter-markdown-loader": "^3.7.0", "frontmatter-markdown-loader": "^3.7.0",
"husky": "^9.0.11", "husky": "^9.0.11",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lint-staged": "^15.5.1", "showdown": "^2.1.0",
"next-sitemap": "^4.0.9", "standard": "^17.1.0"
"prettier": "3.5.3",
"react-test-renderer": "^18.3.1",
"showdown": "^2.1.0"
} }
} }

59
public/admin/config.yml Normal file
View file

@ -0,0 +1,59 @@
local_backend: true
backend:
name: git-gateway
branch: main # Branch to update (optional; defaults to master)
media_folder: public/img
public_folder: img
collections:
- name: "recipes"
label: "Recipes"
folder: "content/recipes"
create: true
fields:
- {label: "Title", name: "title", widget: "string"}
- {label: "Description", name: "description", widget: "string"}
- {label: Image, name: image, widget: image, required: false}
- {label: "You Will Need", name: "you-will-need", widget: "markdown" }
- {label: "Recipe", name: "body", widget: "markdown" }
- name: "pages"
label: "Pages"
files:
- label: "CV"
name: "cv"
file: "content/pages/cv.yml"
fields:
- label: Core competencies
widget: list
name: competencies
allow_add: true
- label: Education history
widget: list
name: education
allow_add: true
- label: Certifications
widget: list
name: certifications
allow_add: true
- label: Languages
widget: list
name: languages
allow_add: true
fields:
- {label: "Name", name: name, widget: string }
- {label: "Proficiency", name: proficiency, widget: string }
- label: Technical skills
widget: list
name: tech-skills
- label: Professional experience
name: experience
widget: list
fields:
- { label: Position, name: position, widget: string }
- { label: Employer, name: employer, widget: string }
- { label: Start date, name: start, widget: string }
- { label: End date, name: end, widget: string }
- { label: Description, name: desc, widget: markdown }

13
public/admin/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Content Manager</title>
<!-- <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script> -->
</head>
<body>
<!-- Include the script that builds the page and powers Decap CMS -->
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

1
public/next.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,10 +0,0 @@
# *
User-agent: *
Allow: /
# Host
Host: https://www.aaronjy.me
# Sitemaps
Sitemap: https://www.aaronjy.me/sitemap.xml
Sitemap: https://www.aaronjy.me/server-sitemap-index.xml

View file

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://www.aaronjy.me/cv</loc><priority>0.8</priority></url>
<url><loc>https://www.aaronjy.me/library</loc><priority>0.8</priority></url>
<url><loc>https://www.aaronjy.me/tags</loc><priority>0.8</priority></url>
<url><loc>https://www.aaronjy.me/writing</loc><priority>0.8</priority></url>
<url><loc>https://www.aaronjy.me/library/the-alchemist</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-invisible-man</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/wintering-the-power-of-rest-and-retreat-in-difficult-times</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-time-machine</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/when-the-moon-hits-your-eye</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-song-of-achilles</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/to-be-taught-if-fortunate</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/on-tyranny</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-dangers-of-smoking-in-bed</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-midnight-library</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/a-night-to-remember</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/sex-punishment</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/a-monster-calls</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/diary-of-an-oxygen-thief</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/1984</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/alices-adventures-in-wonderland</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-nature-of-alexander</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/eleven-kinds-of-loneliness</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/star-maker</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/stray-reflections</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/stasiland-stories-from-behind-the-berlin-wall</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/cities-that-shaped-the-ancient-world</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/animal-farm</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/a-wizard-of-earthsea</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/one-thousand-and-one-nights-a-retelling</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-tombs-of-atuan</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/childhoods-end</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/library/the-farthest-shore</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/about</loc><priority>0.8</priority></url>
<url><loc>https://www.aaronjy.me</loc><priority>1</priority></url>
<url><loc>https://www.aaronjy.me/writing/migrating-from-github-to-forgejo</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/performance-considerations-when-writing-a-tcp-game-server-in-dotnet</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/deploying-aaronjy-me-on-a-google-storage-bucket</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/supporting-content-file-structure-changes-on-a-static-site</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/attitudes-to-reading-and-how-mine-have-changed</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/a-reflection-on-wintering-by-katherine-may</loc><priority>0.6</priority></url>
<url><loc>https://www.aaronjy.me/writing/how-my-adhd-makes-handling-relationships-difficult</loc><priority>0.6</priority></url>
</urlset>

View file

@ -1,41 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: BCPG v1.63
mQSuBGYs08wRDACGpe++RJHa0CmDnvo3001U19EUxv5xDo1cJk6crcX91KD2HrZu
jO4/MGC3N93v/6NvvRgv9gxYqr4tvcc4euj1WbsyNJ5HEsDXkoi5ibUAQmDRQyIj
l2AR5mDbrwXXRTxGgItoqK+mUqPCQL3OBRa1EMHcL28IhLS/bvhviA1My0u83B3d
qFUm6lli4SIAOmubBytIF8kulx5Vn6SwXzlfmpJ7kyFr2dmd9tAF2l28zL/L2qbj
6Q03EmCpvcWshiRuUjt8VnuBH+nuuY3YkEzCFZwa0kryKcoWpsQja7nACMZInWAr
oTLAZVI9mJx8qnA8j8+gKE5rnkDibKkMKifiA9xX/392ufnxQ09sL0vq11d6iWFz
v2hyq0h6Pj1MZ+QgQnVBmJiLnAQZq+B2OZcUPBD/pk+vX4FAqbfAATbLoZ1x/M3l
kbmYvzgGNIowH+VaR8FEn3RGx/sqMsLihy7oanQVbBHWRvT+M7D7t8VCRkBkFb+X
8OHAVsANbrg4O68BAP908xlKj9XOpQMr39FRPP3ZpbtMZA1FI9eh9POWvtgZC/9P
wJDKnugYjV4rlMdiaAEFYkw9FEIWBKfjqvJbg0OedCz85qAqZZhLcEKz68J+PfIC
QwnehW24CQMG3lFYlO/cXGz4HsYAQNkphc5RupksvnhLJxcf7ZOu79AMn2gxTW/u
TnMGsJLD2S/ieToetG+xIXhAJ5+KSON+kMK1joxxJhwM3bYEQLRwtWv1EQjhFci0
tEm19dp625MIWh6mometDRDobRwRdcsoD0xUY2mVQwDER0TjqUlJVvyS7QhvQHmg
REFDdIaQbFpx6Ri3XTrHBX/2Z4Aaw/4m8ARZ8wXdN5f6ZjAgWmzBWMJyYst6mqeZ
jZihmhz2CffknrveUSvBqv42As1feqScsmZZh5NjVMOpqBTToaWddKTSlmeg77kC
x8k8lgZXQtGTpCKX2dWDfx63RbmvT3CeulK72a/UmkfgCvw0VRFE7d/nEr4yhg+1
wxrI8l82HlEs5D1Gd9SiGlF7Naq54Hs7Njncmgx9//emYdftaommYDburHpu+E4L
/12+HpIrPyAkUv1m58BcHpgRRoM1xD/3bIni21LXl/k2DLFhEQj7r4z405htdy43
HYuneVGOYvM4ja3bOn8XSJyHFXLN5s6w2GrNgEo0cPtXLbAWiaC4gucxIkRv/LW4
063qZ6sRIhzRgmSogGwhS5WJ09enxXj9N1EaIXmUH8AVStJbg72ema7PzZJkOWHK
y12dTQJvJSFNnU2e4kny/XHZxk7rBOk7N6jEuCci1EbUj7+GSR5BqDm2TnrqQkAg
BGCOnfQ/vOyjF8YWh5nBy66zQ/meTy5gzainDi8XOPRlmf1cqoQEoLdEh/MMY3pW
W8Bd6wKt5NCnVSPWB1f48jXJrgx0iUR58EWqnCvDjojZlqI7uZUI7z5+Q3b4p8eF
jqErsF1EKEaHBZcgb4ScwomKfZRIZj1ciPveLKQB4jd7bNN7GlICPJi25KKHemqy
CIFWUYxJRWVbhGpMVkR/4IJmFuqj01/ApbZZVATDBRl2jdMBcIQ58TH0W6G/qULg
k7QgQWFyb24gWWFyYm9yb3VnaCA8bWVAYWFyb25qeS5tZT6IXgQTEQoABgUCZizT
zAAKCRB2asy1Uw4clqXGAP0YS2fW78sDrsUKP/9G67+PNYmf7x3hgz/wHPctdZF6
MAD/X5q9506+vYoxPzIjg/WQs393eFhEPgv7FP+zI+8iJXK5AQ0EZizTzBAEAP//
////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+V
GbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44
a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AAICA/0YD77y3YkEP8zVCDDi
Tfjco7NOWoHSSbk0q8iHhrbfN9RMOQQVs1FXHCInyXGmmTc79g5PjqIN5n/+agXS
0BGwbygN0/hMh0EnvmMLCfzRTAPufInrDN4/tKVrGxXMAT9Gnh1RDjSBzh7H/0vr
63UNVLiQeRX1JIXAbuWB1dp6D4heBBgRCgAGBQJmLNPMAAoJEHZqzLVTDhyWxMEB
AL0AMD/6SZsCgV6wV2NsCD/6TnErJjIiszJNSVOw4vpuAP4oriEQ2zTw1leT9DKA
bRZce4ZhaHXiVBufyfiPIC3Jsw==
=7Ll3
-----END PGP PUBLIC KEY BLOCK-----

1
public/vercel.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View file

@ -1,40 +0,0 @@
import { formatDate } from "@/lib/helpers";
import { NextSeo } from "next-seo";
import Link from "next/link";
import React, { useEffect } from "react";
import "highlight.js/styles/atom-one-dark.css";
import hljs from "highlight.js";
function Article({ title, excerpt, datePublished, tags, html }) {
useEffect(() => {
hljs.highlightAll();
}, [html]);
return (
<>
<h1>{title}</h1>
<article>
<NextSeo
title={title}
description={excerpt}
openGraph={{
title,
description: excerpt,
type: "article",
article: {
publishedTime: datePublished ?? null,
},
}}
/>
<div>
<Link href="./">Back...</Link>
{datePublished && <p>{formatDate(datePublished)}</p>}
<div data-test="content" dangerouslySetInnerHTML={{ __html: html }} />
{tags && <p>Tags: {tags.join(", ")}</p>}
</div>
</article>
</>
);
}
export default Article;

View file

@ -1,80 +0,0 @@
import { formatDate } from "@/lib/helpers";
import { NextSeo } from "next-seo";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import style from "./BookReview.module.css";
import ExternalLink from "../ExternalLink/ExternalLink";
function BookReview({ review, html }) {
const { title, image, author, description, url, tags, rating, readDate } =
review;
const imageUrl = image
? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}`
: undefined;
return (
<>
<h1>
{title} <small>- {author}</small>
</h1>
<Link href="./">Back...</Link>
<article>
<NextSeo
title={title}
description={description}
openGraph={{
title,
description,
type: "article",
article: {
publishedTime: readDate ?? null,
},
}}
/>
<div>
<div className={style.layout}>
{imageUrl && (
<Image
src={imageUrl}
width={250}
height={580}
alt=""
className={style.thumbnail}
/>
)}
<div>
<div
data-test="content"
dangerouslySetInnerHTML={{
__html: html || "<p>(no review)</p>",
}}
/>
<p>
{tags?.length && (
<>
<span className="bold">Genres:</span>&nbsp;{tags.join(",")}
<br />
</>
)}
<span className="bold">Rating:</span>&nbsp;{rating}/5
<br />
<span className="bold">Read on:</span>&nbsp;
{formatDate(readDate)}
</p>
<p>
<ExternalLink href={url}>View on The StoryGraph</ExternalLink>
</p>
</div>
</div>
</div>
</article>
</>
);
}
export default BookReview;

View file

@ -1,33 +0,0 @@
.layout {
display: flex;
flex-direction: row;
gap: 1.25rem;
margin: 1.25rem 0;
}
.layout:first-child {
flex-grow: 0;
flex-shrink: 0;
}
.layout:last-child {
flex-grow: 1;
}
.layout p:first-child {
margin-top: 0;
}
.thumbnail {
border-radius: 8px;
}
@media screen and (max-width: 650px) {
.layout {
flex-direction: column-reverse;
}
.thumbnail {
width: 100%;
}
}

View file

@ -1,57 +0,0 @@
import style from "./BookReviewItem.module.css";
import Link from "next/link";
export default function BookReviewItem({
href,
title,
author,
rating,
image,
tags,
}) {
const imageUrl = image
? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}`
: undefined;
return (
<div className={style.item}>
<Link href={href}>
<div
className={style.thumb}
style={{
backgroundImage: `url(${imageUrl ?? "/img/book-placeholder.jpg"})`,
}}
>
<div className={style.rating}>
<Star />
&nbsp;{rating}
</div>
</div>
</Link>
<div>
<h2 className={style.heading}>
<Link href={href}>{title}</Link>
</h2>
<p className={style.author}>{author}</p>
{tags?.length && <p>{tags.join(", ")}</p>}
</div>
</div>
);
}
function Star() {
return (
<svg
style={{
fill: "currentColor",
}}
height="15"
width="15"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 47.94 47.94"
xmlSpace="preserve"
>
<path d="m26.285 2.486 5.407 10.956a2.58 2.58 0 0 0 1.944 1.412l12.091 1.757c2.118.308 2.963 2.91 1.431 4.403l-8.749 8.528a2.582 2.582 0 0 0-.742 2.285l2.065 12.042c.362 2.109-1.852 3.717-3.746 2.722l-10.814-5.685a2.585 2.585 0 0 0-2.403 0l-10.814 5.685c-1.894.996-4.108-.613-3.746-2.722l2.065-12.042a2.582 2.582 0 0 0-.742-2.285L.783 21.014c-1.532-1.494-.687-4.096 1.431-4.403l12.091-1.757a2.58 2.58 0 0 0 1.944-1.412l5.407-10.956c.946-1.919 3.682-1.919 4.629 0z" />
</svg>
);
}

View file

@ -1,39 +0,0 @@
.item {
height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.heading {
font-size: 1rem;
margin-bottom: 5px;
}
.author {
margin-top: 5px;
/* font-weight: bold; */
}
.thumb {
width: auto;
height: 290px;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
border-radius: 6px;
position: relative;
}
.rating {
position: absolute;
right: 0.5rem;
bottom: 0.5rem;
background-color: var(--light);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: flex;
flex-direction: row;
align-items: center;
pointer-events: none;
}

56
src/components/CV/CV.jsx Normal file
View file

@ -0,0 +1,56 @@
import React from 'react'
import style from './CV.module.css'
function CV ({ competencies, education, certifications, languages, experience }) {
return (
<div className={style.cv}>
<div>
<h2>Core competencies</h2>
<ul>
{competencies.sort().map(c => <li key={c}>{c}</li>)}
</ul>
<h2>Certifications</h2>
<ul>
{certifications.sort().map(c => <li key={c}>{c}</li>)}
</ul>
<h2>Languages</h2>
<ul>
{languages.sort().map(c => <li key={c}>{c.name} - {c.proficiency}</li>)}
</ul>
<h2>Education history</h2>
<p>{education}</p>
</div>
<div>
<h2>Professional experience</h2>
{experience.map((exp, i) => (
<CVWorkExperience key={i} employer={exp.employer} position={exp.position} start={exp.start} end={exp.end}>{exp.desc}</CVWorkExperience>
))}
</div>
</div>
)
}
export default CV
function CVWorkExperience ({ position, employer, start, end, children }) {
return (
<div className={style['work-experience']}>
<div>
<h3>
{position}
<br />
<small>{employer}</small>
</h3>
<div>
<time>{start}</time>-<time>{end}</time>
</div>
</div>
<div dangerouslySetInnerHTML={{ __html: children }} />
</div>
)
}

View file

@ -0,0 +1,29 @@
.cv {
display: flex;
flex-direction: row;
gap: 25px;
}
.cv > div:first-child {
flex: 1;
}
.cv > div:last-child {
flex: 2;
}
.cv .work-experience >div:first-child {
display: flex;
align-items: center;
justify-content: space-between;
}
.cv ul {
margin-left: 0;
list-style: inside;
list-style-type: circle;
}
.cv ul li:not(:last-child) {
margin-bottom: 10px;
}

View file

@ -0,0 +1,31 @@
import React from 'react'
/**
* Sets default values for external links on an anchor tag.
* @returns
*/
function ExternalLink ({
href,
rel = 'nofollow noopener',
children,
target = '_blank'
}) {
return (
<>
<a href={href} rel={rel} target={target}>
<svg
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
style={{ display: 'inline', width: '1rem', marginRight: '0.25rem' }}
>
<g>
<path fill='currentColor' d='M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z' />
</g>
</svg>
{children}
</a>
</>
)
}
export default ExternalLink

View file

@ -1,34 +0,0 @@
import React from "react";
/**
* Sets default values for external links on an anchor tag.
* @returns
*/
function ExternalLink({
href,
rel = "nofollow noopener",
children,
target = "_blank",
}) {
return (
<>
<a href={href} rel={rel} target={target}>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
style={{ display: "inline", width: "1rem", marginRight: "0.25rem" }}
>
<g>
<path
fill="currentColor"
d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z"
/>
</g>
</svg>
{children}
</a>
</>
);
}
export default ExternalLink;

View file

@ -1,44 +1,47 @@
import React from "react"; import React from 'react'
import style from "./Footer.module.css"; import style from './Footer.module.css'
// @ts-ignore function Footer () {
import pck from "../../../package.json";
function Footer() {
return ( return (
<footer className={`${style.footer} hide-print`} data-testid="footer"> <footer className={style.footer}>
<hr />
<nav> <nav>
<div> <ul>
<span> <li>
<a href="#">Back to top</a> <a href='#'>Back to top</a>
</span> </li>
{", "} <li>
<span> <a href='mailto:me@aaronjy.me'>Send me an email</a>
<a href="/static/pgp.txt">PGP key</a> </li>
</span> </ul>
{", "} </nav>
<span>
<a
href="https://git.aaronjy.me/aaron/aaronjy-me"
target="_blank"
rel="nofollow noopener noreferrer"
>
View site src
</a>
</span>
</div>
<div> <nav>
<small> <ul>
2025 Aaron Yarborough,{" "} <li>
<span title="major.minior.patch.content">v{pck.version}</span> <small>
</small> 2024 Aaron Yarborough, made with{' '}
</div> <a
target='_blank'
rel='nofollow noopener noreferrer'
href='https://nextjs.org/'
>
Next.js
</a>{' '}
and{' '}
<a
target='_blank'
rel='nofollow noopener noreferrer'
href='https://yegor256.github.io/tacit/'
>
Tacit
</a>
</small>
</li>
</ul>
</nav> </nav>
</footer> </footer>
); )
} }
export default Footer; export default Footer

View file

@ -0,0 +1,3 @@
.footer nav:first-child a {
text-transform: lowercase;
}

View file

@ -1,5 +1,11 @@
import style from "./Grid.module.css"; import React from 'react'
export default function Grid({ columns, children }) { import style from './Grid.module.css'
return <div className={style.grid}>{children}</div>;
function Grid ({ children }) {
return (
<div className={style.grid}>{children}</div>
)
} }
export default Grid

View file

@ -1,5 +1,5 @@
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(3, 1fr);
gap: 1rem; gap: 32px;
} }

View file

@ -1,26 +1,18 @@
import Link from "next/link"; import Link from 'next/link'
import React from "react"; import React from 'react'
import styles from "./Header.module.css"; import styles from './Header.module.css'
function Header() { function Header () {
return ( return (
<header className={`${styles.header} hide-print`} data-testid="header"> <header className={styles.header}>
<nav> <nav>
<Link href="/">Home</Link> <Link href='/'>Home</Link>
{", "} <Link href='/writing'>Writing</Link>
<Link href="/writing">Writing</Link> <Link href='/cv'>CV</Link>
{", "}
<Link href="/tags">Tags</Link>
{", "}
<Link href="/cv">CV</Link>
{", "}
<Link href="/library">Library</Link>
{", "}
<Link href="/about">About</Link>
</nav> </nav>
</header> </header>
); )
} }
export default Header; export default Header

View file

@ -1,3 +1,9 @@
.header { .header nav {
margin-top: 20px; display: flex;
justify-content: center;
gap: 20px;
} }
.header a {
text-transform: lowercase;
}

View file

@ -1,3 +0,0 @@
export default function Loading() {
return <p>Loading...</p>;
}

View file

@ -1,144 +0,0 @@
import React from "react";
import style from "./Resume.module.css";
import { markdownToHtml } from "@/services/content-service";
import { MDXClient } from "next-mdx-remote-client/csr";
import Link from "next/link";
function Resume({
introMdxSource,
competencies,
education,
certifications,
languages,
experience,
}) {
return (
<div className={style.cv}>
<ol className="hide-print">
<li>
<a href="#experience">Professional experience</a>
</li>
<li>
<a href="#competencies">Competencies</a>
</li>
<li>
<a href="#competencies">Certifications</a>
</li>
<li>
<a href="#languages">Languages</a>
</li>
<li>
<a href="#education">Education</a>
</li>
</ol>
<div>
<div className={style.about}>
<MDXClient {...introMdxSource} />
<span className="print-only">
See more at <a href="https://aaronjy.me/cv">aaronjy.me/cv</a>
</span>
</div>
</div>
<div className={style.cvContent}>
<div className={style.experience}>
<h2 id="experience">Professional experience</h2>
{experience?.map((exp, i) => (
<div key={i}>
<WorkExperience
employer={exp.employer}
position={exp.position}
start={exp.start}
end={exp.end}
skills={exp.skills}
>
{markdownToHtml(exp.description)}
</WorkExperience>
</div>
))}
</div>
<div className={style.sidebar}>
<h2 id="competencies">Competencies</h2>
<ul>
{competencies
?.sort((a, b) => a.name - b.name)
.map((c, i) => (
<li key={i}>{c.name}</li>
))}
</ul>
<h2 id="certifications">Certifications</h2>
<ul>
{certifications
?.sort((a, b) => a.name > b.name)
.map((c, i) => (
<li key={i}>{c.name}</li>
))}
</ul>
<h2 className="languages">Languages</h2>
<ul>
{languages?.sort().map((c, i) => (
<li key={i}>
<strong>{c.name}</strong> - {c.proficiency}
</li>
))}
</ul>
<h2 className="education">Education</h2>
<ul>
{education
?.sort((a, b) => a.name - b.name)
.map((c, i) => (
<li key={i}>{c.name}</li>
))}
</ul>
</div>
</div>
</div>
);
}
export default Resume;
function WorkExperience({ position, employer, start, end, skills, children }) {
return (
<>
<table className={style.experienceTable}>
<tbody>
<tr>
<td>
<span id={position} className={style.position}>
{position}
</span>
<br />
<span className={style.employer}>{employer}</span>
</td>
<td className="text-right">
<time>{start}</time> - <time>{end}</time>
</td>
</tr>
<tr>
<td colSpan={2}>
<div
data-test="children"
dangerouslySetInnerHTML={{ __html: children }}
className={style.experienceContent}
/>
{!!skills?.length && (
<div className={style.skillList}>
{skills.sort().map((skill) => (
<span key={skill}>{skill}</span>
))}
</div>
)}
</td>
</tr>
</tbody>
</table>
<hr />
</>
);
}

View file

@ -1,74 +0,0 @@
.experienceTable {
width: 100%;
}
.position {
font-weight: 700;
}
.cvContent {
display: flex;
flex-direction: row-reverse;
gap: 0.5em;
position: relative;
}
.cvContent .experience {
flex-basis: 66.66%;
}
.cvContent .sidebar {
flex-basis: 33.33%;
position: sticky;
top: 10px;
left: 0px;
height: 1px;
/* Needed to make sticky work */
}
.about {
margin-bottom: 0;
}
.cvContent ol,
.cvContent ul {
padding: 0 1rem;
}
.skillList {
font-size: 0.8em;
display: flex;
flex-wrap: wrap;
gap: 0.2em;
}
.skillList span {
background-color: var(--dark);
color: var(--light);
padding: 0 0.5em;
}
.experienceContent > p {
margin-bottom: 0;
}
@media screen and (max-width: 768px) {
.cvContent {
display: block;
}
.cvContent .sidebar {
height: auto;
position: relative;
}
}
@media print {
.cvContent .experience {
flex-basis: 80%;
}
.cvContent .sidebar {
flex-basis: 20%;
}
}

View file

@ -1,17 +0,0 @@
import { formatDate } from "@/lib/helpers";
import Link from "next/link";
export default function StaticContentList({ entries, urlPrefix, max = 0 }) {
return (
<div>
{entries
.map((e) => (
<p key={e.slug}>
<Link href={`${urlPrefix}${e.slug}`}>{e.title}</Link>{" "}
{!!e.datePublished && <i>{`on ${formatDate(e.datePublished)}`}</i>}
</p>
))
.slice(0, max > 0 ? max : entries.length)}
</div>
);
}

View file

@ -1,56 +0,0 @@
// Posts
export class FailedFetchPostsError extends Error {
constructor(msg) {
super(`Failed to fetch posts: ${msg}`);
this.name = "FailedFetchPostsError";
}
}
export class FailedFetchPostError extends Error {
constructor(slug, msg) {
super(`Failed to fetch post '${slug}: ${msg}`);
this.name = "FailedFetchPostError";
}
}
// Book reviews
export class FailedFetchBookReviewsError extends Error {
constructor(msg) {
super(`Failed to fetch book reviews: ${msg}`);
this.name = "FailedFetchBookReviewsError";
}
}
export class FailedFetchBookReviewError extends Error {
constructor(slug, msg) {
super(`Failed to fetch book review '${slug}: ${msg}`);
this.name = "FailedFetchBookReviewError";
}
}
// Basic pages
export class FailedFetchBasicPagesError extends Error {
constructor(msg) {
super(`Failed to fetch basic pages: ${msg}`);
this.name = "FailedFetchBasicPagesError";
}
}
export class FailedFetchBasicPageError extends Error {
constructor(slug, msg) {
super(`Failed to fetch basic page '${slug}: ${msg}`);
this.name = "FailedFetchBasicPageError";
}
}
// CV
export class FailedFetchCVError extends Error {
constructor(msg) {
super(`Failed to fetch basic pages: ${msg}`);
this.name = "FailedFetchCVError";
}
}

View file

@ -1,17 +1,21 @@
import React from "react"; import React from 'react'
import style from "./DefaultLayout.module.css"; import style from './DefaultLayout.module.css'
import Header from "@/components/Header/Header"; import Header from '@/components/Header/Header'
import Footer from "@/components/Footer/Footer"; import Footer from '@/components/Footer/Footer'
function DefaultLayout({ children }) { import { Barlow } from 'next/font/google'
const fontMain = Barlow({ subsets: ['latin'], weight: ['400', '600'] })
function DefaultLayout ({ children }) {
return ( return (
<main className={`${style.layout}`}> <div className={style.layout}>
<Header /> <Header />
<>{children}</> <main className={`${fontMain.className}`}>{children}</main>
<Footer /> <Footer />
</main> </div>
); )
} }
export default DefaultLayout; export default DefaultLayout

View file

@ -0,0 +1,9 @@
.layout {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.layout main {
flex-grow: 1;
}

View file

@ -1,27 +0,0 @@
import * as dateFns from "date-fns";
export function filenameToSlug(input) {
return stringToSlug(input.substring(0, input.indexOf(".")));
}
export function stringToSlug(str) {
return str
.trim()
.toLowerCase()
.replace(/[\W_]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function formatDate(date) {
return dateFns.format(Date.parse(date), "PPP");
}
/**
* Silliness to make sure dates don't get passed to the
* page function below as [object Object]
* @param {*} obj
* @returns
*/
export function stringifyAndParse(obj) {
return JSON.parse(JSON.stringify(obj));
}

View file

@ -1,44 +0,0 @@
import StaticContentList from "@/components/StaticContentList/StaticContentList";
import { fetchPosts } from "@/services/content-service";
import { useEffect, useState, useTransition } from "react";
export const mdxComponents = {
StaticContentList: ({ type, urlPrefix, max = 0 }) => {
const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(false);
useEffect(
function () {
if (!urlPrefix || max <= 0) return;
switch (type) {
case "posts":
(async function () {
setLoading(true);
const res = await fetchPosts([]);
const json = await res.json();
setItems(
json.data.sort((a, b) => a.datePublished < b.datePublished) ??
[],
);
setLoading(false);
})();
break;
default:
throw `Could not render StaticContentList: content type ${type} not supported.`;
}
},
[type, urlPrefix, max],
);
return (
<>
{isLoading && <p>Loading...</p>}
{!isLoading && (
<StaticContentList entries={items} urlPrefix={urlPrefix} max={max} />
)}
</>
);
},
};

View file

@ -1,94 +0,0 @@
import Loading from "@/components/Loading/Loading";
import { FailedFetchBasicPagesError } from "@/errors";
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import { mdxComponents } from "@/lib/mdx-components";
import { fetchBasicPages } from "@/services/content-service";
import { MDXClient } from "next-mdx-remote-client";
import { serialize } from "next-mdx-remote-client/serialize";
import { NextSeo } from "next-seo";
import { useRouter } from "next/router";
export async function getStaticPaths() {
const res = await fetchBasicPages(["path"]);
if (!res.ok) {
throw new FailedFetchBasicPagesError(await res.text());
}
const pages = (await res.json()).data;
return {
paths: pages.map((page) => ({
params: {
path: [page.path ?? ""],
},
})),
fallback: true,
};
}
export async function getStaticProps({ params }) {
const { path } = params;
const res = await fetchBasicPages([], {
path: {
_eq: path?.join("/") ?? null,
},
});
if (!res.ok) {
throw new FailedFetchBasicPageError(path, await res.text());
}
const page = (await res.json()).data.at(0);
if (!page) {
return {
notFound: true,
revalidate: 60,
};
}
const { content, title } = page;
const mdxSource = await serialize({ source: content });
return {
props: {
title,
mdxSource,
},
revalidate: 60,
};
}
export default function BasicPage({ title, mdxSource }) {
const { isFallback } = useRouter();
if (isFallback) {
return (
<DefaultLayout>
<Loading />
</DefaultLayout>
);
}
if (!mdxSource || "error" in mdxSource) {
return <p>Something went wrong: {mdxSource?.error ?? "???"}</p>;
}
return (
<DefaultLayout>
<NextSeo
title={title}
description={undefined}
openGraph={{
title,
description: undefined,
}}
/>
<div>
<MDXClient {...mdxSource} components={mdxComponents} />
</div>
</DefaultLayout>
);
}

View file

@ -1,14 +1,12 @@
import { DefaultSeo } from "next-seo"; import '@/styles/globals.css'
import "@/styles/globals.css";
export default function App({ Component, pageProps }) { import { DefaultSeo } from 'next-seo'
export default function App ({ Component, pageProps }) {
return ( return (
<> <>
<DefaultSeo <DefaultSeo defaultTitle='Aaron Yarborough' titleTemplate='%s | Aaron Yarborough' />
defaultTitle="Aaron Yarborough"
titleTemplate="%s | Aaron Yarborough"
/>
<Component {...pageProps} /> <Component {...pageProps} />
</> </>
); )
} }

View file

@ -1,20 +1,15 @@
import { Html, Head, Main, NextScript } from "next/document"; import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() { export default function Document () {
return ( return (
<Html lang="en"> <Html lang='en'>
<Head> <Head>
<link rel="stylesheet" href="https://neat.joeldare.com/neat.css" /> <link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/yegor256/tacit@gh-pages/tacit-css-1.7.1.min.css' />
<script
defer
data-domain="aaronjy.me"
src="https://analytics.aaronjy.me/js/script.js"
/>
</Head> </Head>
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
); )
} }

View file

@ -1,74 +1,25 @@
import Resume from "@/components/Resume/Resume"; import CV from '@/components/CV/CV'
import { FailedFetchCVError } from "@/errors"; import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout"; import React from 'react'
import { fetchCV } from "@/services/content-service"; import yaml from 'js-yaml'
import { NextSeo } from "next-seo"; import fs from 'fs'
import showdown from 'showdown'
import style from "./cv.module.css"; function CVPage ({
import { serialize } from "next-mdx-remote-client/serialize";
export const Title = "CV";
export async function getStaticProps() {
const res = await fetchCV([]);
if (!res.ok) {
throw new FailedFetchCVError(await res.text());
}
const cv = (await res.json()).data;
if (!cv) {
return {
notFound: true,
revalidate: 60,
};
}
const {
intro,
competencies,
education,
languages,
certifications,
experience,
} = cv;
const introMdxSource = await serialize({ source: intro });
return {
props: {
introMdxSource,
competencies,
education,
languages,
certifications,
experience,
},
revalidate: 60,
};
}
export default function ResumePage({
introMdxSource,
competencies, competencies,
education, education,
certifications, certifications,
languages, languages,
experience, experience
}) { }) {
return ( return (
<DefaultLayout> <DefaultLayout>
<NextSeo
title={Title}
openGraph={{
title: Title,
}}
/>
<h1 className="hide-print">{Title}</h1>
<h1 className={`${style.printHeading} print-only`}>Aaron Yarborough</h1>
<section> <section>
<Resume <h1>CV</h1>
introMdxSource={introMdxSource} </section>
<section>
{/* eslint-disable-next-line react/jsx-pascal-case */}
<CV
competencies={competencies} competencies={competencies}
education={education} education={education}
certifications={certifications} certifications={certifications}
@ -77,5 +28,26 @@ export default function ResumePage({
/> />
</section> </section>
</DefaultLayout> </DefaultLayout>
); )
} }
export function getStaticProps () {
const content = fs.readFileSync('./content/pages/cv.yml', {
encoding: 'utf-8'
})
const data = yaml.load(content)
const MDConverter = new showdown.Converter()
data.experience = data.experience.map((exp) => ({
...exp,
desc: MDConverter.makeHtml(exp.desc)
}))
return {
props: { ...data }
}
}
export default CVPage

76
src/pages/index.js Normal file
View file

@ -0,0 +1,76 @@
import Head from 'next/head'
import ExternalLink from '@/components/ExternalLink'
import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
export default function Home () {
return (
<DefaultLayout>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel='icon' href='/favicon.ico' />
</Head>
<section>
<h1>Hello!</h1>
<p>
I&apos;m Aaron. I&apos;m a Brit living in Newcastle-upon-tyne, UK. I
work professionally as a Software Engineer and Tutor, and study
languages in my spare time.
</p>
<p>
This is my little corner of the web! I&apos;ve always had a habit of
&apos;lurking&apos; online; I barely interact with the content I
consume, and you&apos;ll rarely if ever catch me posting or commenting
on something. That said, this little site endeavours to pull me by my
ankles out of the weeds in the great digital park we find ourselves,
and encourage me to share a bit more about myself online.
</p>
</section>
<section>
<h2>Where to find me</h2>
<ul>
<li>
<strong>
<ExternalLink href='https://letterboxd.com/aaronyarbz/'>
Letterboxd
</ExternalLink>
</strong>{' '}
is a social platform for film lovers to rate, review, and discover
movies, akin to &quot;Goodreads for film.&quot;
</li>
<li>
<strong>
<ExternalLink href='https://github.com/AaronJY'>
GitHub
</ExternalLink>
</strong>{' '}
is a web-based platform for version control and collaboration on
software development projects. Find out what I&apos;ve been working
on here!
</li>
<li>
<strong>
<ExternalLink href='https://www.linkedin.com/in/aaronjyarborough/'>
LinkedIn
</ExternalLink>
</strong>
, unfortunately. A social network for professionals.
</li>
</ul>
{/* <p>
<strong>Yarbz Tutoring</strong> is my tutoring site. You can read a
bit more about my tutoring and general software develpment experience,
and book a class if it suits you.
</p>
<p>
<ExternalLink href='https://tutoring.yarbz.digital'>
Read more about my tutoring side-gig.
</ExternalLink>
</p> */}
</section>
</DefaultLayout>
)
}

View file

@ -1,74 +0,0 @@
import React from "react";
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import BookReview from "@/components/Book/BookReview";
import { fetchBookReviews, markdownToHtml } from "@/services/content-service";
import {
FailedFetchBookReviewError,
FailedFetchBookReviewsError,
} from "@/errors";
import { useRouter } from "next/router";
import Loading from "@/components/Loading/Loading";
export async function getStaticPaths() {
const res = await fetchBookReviews(["slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchBookReviewsError(await res.text());
}
const reviews = (await res.json()).data;
return {
paths: reviews.map((post) => ({
params: {
slug: post.slug,
},
})),
fallback: true,
};
}
export const getStaticProps = async ({ params }) => {
const { slug } = params;
const res = await fetchBookReviews([], {
slug,
status: "published",
});
if (!res.ok) {
throw new FailedFetchBookReviewError(slug, await res.text());
}
const review = (await res.json()).data.at(0);
if (!review) {
return {
notFound: true,
revalidate: 60,
};
}
const content = review.review;
const html = markdownToHtml(content);
return {
props: {
review,
html,
},
revalidate: 60,
};
};
export default function LibrarySingle({ review, html }) {
const { isFallback } = useRouter();
return (
<DefaultLayout>
{!isFallback && <BookReview review={review} html={html} />}
{isFallback && <Loading />}
</DefaultLayout>
);
}

View file

@ -1,55 +0,0 @@
import BookReviewItem from "@/components/BookReviewItem/BookReviewItem";
import Grid from "@/components/Grid/Grid";
import { FailedFetchBookReviewsError } from "@/errors";
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import { stringifyAndParse } from "@/lib/helpers";
import { fetchBookReviews } from "@/services/content-service";
import { NextSeo } from "next-seo";
export const Title = "Library";
export async function getStaticProps() {
const res = await fetchBookReviews();
if (!res.ok) {
throw new FailedFetchBookReviewsError(await res.text());
}
const reviews = (await res.json()).data.sort(
(a, b) => new Date(b.readDate).getTime() - new Date(a.readDate).getTime(),
);
return {
props: {
reviews: stringifyAndParse(reviews),
},
revalidate: 60,
};
}
export default function Library({ reviews }) {
return (
<DefaultLayout>
<NextSeo
title={Title}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
<section>
<Grid columns={5}>
{reviews.map((review, _) => (
<BookReviewItem
key={review.title}
{...review}
href={`/library/${review.slug}`}
/>
))}
</Grid>
</section>
</DefaultLayout>
);
}

View file

@ -0,0 +1,46 @@
// import Grid from '@/components/Grid/Grid'
// import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
// import React from 'react'
// import path from 'path'
// import fs from 'fs'
// function Recipes ({ recipes }) {
// return (
// <DefaultLayout>
// <section>
// <h1>Recipes</h1>
// <Grid>
// {recipes.length &&
// recipes.map((recipe) => (
// <div key={recipe.name}>{recipe.name ?? 'unknown'}</div>
// ))}
// </Grid>
// </section>
// </DefaultLayout>
// )
// }
// export default Recipes
// export async function getStaticProps () {
// const recipeDirents = await fs.promises
// .readdir('./content/recipes', {
// recursive: false,
// withFileTypes: true
// })
// const recipes = [{
// name: 'lol'
// }]
// for (const recipe of recipeDirents) {
// // const recipePath = path.join('./', recipe.path, recipe.name)
// // const recipeContent = fs.readFileSync(recipePath, { encoding: 'utf-8' })
// console.log(recipeFm)
// }
// return {
// props: { recipes }
// }
// }

View file

@ -1,29 +0,0 @@
import {
fetchBasicPages,
fetchBookReviews,
fetchPosts,
} from "@/services/content-service";
import { getServerSideSitemapIndexLegacy } from "next-sitemap";
const siteUrl = process.env.SITE_URL || "https://www.aaronjy.me";
export const getServerSideProps = async (ctx) => {
const [basicPagesResp, bookReviewsResp, postsResp] = await Promise.all([
fetchBasicPages().then((res) => res.json()),
fetchBookReviews().then((res) => res.json()),
fetchPosts().then((res) => res.json()),
]);
const urls = [
...basicPagesResp.data.map((entry) => `${siteUrl}/${entry.path ?? ""}`),
...bookReviewsResp.data.map(
(entry) => `${siteUrl}/library/${entry.slug ?? ""}`,
),
...postsResp.data.map((entry) => `${siteUrl}/writing/${entry.slug ?? ""}`),
];
return getServerSideSitemapIndexLegacy(ctx, [...urls]);
};
// Default export to prevent next.js errors
export default function SitemapIndex() {}

View file

@ -1,73 +0,0 @@
import { FailedFetchPostsError } from "@/errors";
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import {
fetchItems,
fetchPosts,
getTagsFromPosts,
} from "@/services/content-service";
import { NextSeo } from "next-seo";
import Link from "next/link";
import React from "react";
export async function getStaticProps() {
const res = await fetchPosts(["title", "date_published", "tags", "slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostsError(await res.text());
}
const posts = (await res.json()).data;
const tags = getTagsFromPosts(posts);
return {
props: {
tags,
posts,
},
revalidate: 60,
};
}
export const Title = "Tags";
export default function Tags({ tags, posts }) {
return (
<DefaultLayout>
<NextSeo
title={Title}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
<section>
{Object.keys(tags)
.sort()
.map((tag) => {
const tagPosts = posts
.filter((p) => p.tags.includes(tag))
.sort((a, b) => b.title > -a.title);
return (
<React.Fragment key={tag}>
<h2>{tag}</h2>
<ul>
{tagPosts.map((post, _) => {
return (
<li key={post.slug}>
<Link href={`/writing/${post.slug}`}>{post.title}</Link>
</li>
);
})}
</ul>
</React.Fragment>
);
})}
</section>
</DefaultLayout>
);
}

View file

@ -1,70 +0,0 @@
import React from "react";
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import Article from "@/components/Article/Article";
import { fetchPosts, markdownToHtml } from "@/services/content-service";
import { FailedFetchPostError, FailedFetchPostsError } from "@/errors";
import { useRouter } from "next/router";
import Loading from "@/components/Loading/Loading";
export async function getStaticPaths() {
const res = await fetchPosts(["slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostsError(await res.text());
}
const posts = (await res.json()).data;
return {
paths: posts.map((post) => ({
params: {
slug: post.slug,
},
})),
fallback: true,
};
}
export const getStaticProps = async ({ params }) => {
const { slug } = params;
const res = await fetchPosts([], {
slug,
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostError(slug, await res.text());
}
const post = (await res.json()).data.at(0);
if (!post) {
return {
notFound: true,
revalidate: 60,
};
}
const { content } = post;
const html = markdownToHtml(content);
return {
props: {
post,
html,
},
revalidate: 60,
};
};
export default function WritingSingle({ post, html }) {
const { isFallback } = useRouter();
return (
<DefaultLayout>
{!isFallback && <Article {...post} html={html} />}
{isFallback && <Loading />}
</DefaultLayout>
);
}

View file

@ -1,50 +1,14 @@
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout"; import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
import React from "react";
import { NextSeo } from "next-seo";
import StaticContentList from "@/components/StaticContentList/StaticContentList";
import { fetchPosts } from "@/services/content-service";
import { FailedFetchPostsError } from "@/errors";
export const getStaticProps = async () => { export default function Writing () {
const res = await fetchPosts(["title", "date_published", "slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostsError(await res.text());
}
const json = await res.json();
const posts = json.data.sort(
(a, b) =>
new Date(b.datePublished).getTime() - new Date(a.datePublished).getTime(),
);
return {
props: {
posts,
},
revalidate: 60,
};
};
export const Title = "Writing";
export default function Writing({ posts }) {
return ( return (
<DefaultLayout> <DefaultLayout>
<NextSeo
title={Title}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
<section> <section>
<StaticContentList entries={posts} urlPrefix="writing/" /> <h1>Writing</h1>
</section>
<section>
<i>Nothing to see here yet!</i>
</section> </section>
</DefaultLayout> </DefaultLayout>
); )
} }

View file

@ -1,72 +0,0 @@
import showdown from "showdown";
import camelcaseKeys from "camelcase-keys";
const baseUrl = process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL;
// @ts-ignore
export const fetchPosts = async (...args) => fetchItems("post", ...args);
export const fetchBookReviews = async (...args) =>
fetchItems("book_review", ...args);
export const fetchBasicPages = async (...args) =>
fetchItems("basic_pages", ...args);
export const fetchCV = async (...args) => fetchItems("cv", ...args);
const originalFetch = globalThis.fetch;
globalThis.fetch = async (...args) => {
const response = await originalFetch(...args);
const originalJson = response.json;
response.json = async function () {
const data = await originalJson.call(this);
return camelcaseKeys(data, { deep: true });
};
return response;
};
export async function fetchItems(type, fields = undefined, filter = undefined) {
const url = new URL(`${baseUrl}/items/${type}`);
if (fields?.length) {
url.searchParams.append("fields", fields.join(","));
}
if (filter) {
url.searchParams.append("filter", JSON.stringify(filter));
}
return await apiFetch(url.toString());
}
export function getTagsFromPosts(posts) {
const allTags = {};
for (const post of posts) {
if (!post.tags) {
continue;
}
for (const tag of post.tags) {
allTags[tag] = !allTags[tag] ? 1 : allTags[tag] + 1;
}
}
return allTags;
}
export function markdownToHtml(content) {
const converter = new showdown.Converter({
tables: true,
tablesHeaderId: true,
});
const html = converter.makeHtml(content);
return html;
}
async function apiFetch(...args) {
// @ts-ignore
const res = await fetch(...args);
return res;
}

View file

@ -1,52 +1,8 @@
pre { html, body {
padding: 0; margin: 0;
border-radius: 5px; padding: 0 15px;
} }
tbody { h1, h2, h3 {
vertical-align: top; font-weight: 600;
}
tr {
text-align: left;
}
td:first-child {
padding-right: 20px;
}
.form-group {
margin: 20px 0;
}
.bold {
font-weight: 700;
}
.text-right {
text-align: right;
}
.print-only {
display: none;
}
@media print {
html {
padding-top: 1rem;
}
body {
font-size: 11.5px;
line-height: 1.3;
padding: 0;
}
.hide-print {
display: none;
}
.print-only {
display: initial;
}
} }

View file

View file

@ -1,33 +0,0 @@
import { readdirSync, readFileSync } from "fs";
import path from "path";
import fm from "front-matter";
const dirPath = "./content/books";
const output = [];
const files = readdirSync(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const content = readFileSync(filePath, {
encoding: "utf-8",
});
const { attributes, body } = fm(content, {
allowUnsafe: true,
});
const entry = {
title: attributes.title,
author: attributes.author,
read_date: attributes.readDate,
rating: Math.round(attributes.stars * 2),
// "image": attributes.thumbnailUrl,
tags: attributes.tags.split(", "),
review: body,
url: attributes.url,
};
output.push(entry);
}
console.log(JSON.stringify(output));

View file

@ -1,32 +0,0 @@
import { readdirSync, readFileSync } from "fs";
import path from "path";
import fm from "front-matter";
import { stringToSlug } from "../src/lib/helpers.js";
const dirPath = "./content/writing";
const output = [];
const files = readdirSync(dirPath);
for (const file of files) {
const filePath = path.join(dirPath, file);
const content = readFileSync(filePath, {
encoding: "utf-8",
});
const { attributes, body } = fm(content, {
allowUnsafe: true,
});
const entry = {
slug: stringToSlug(attributes.title),
title: attributes.title,
excerpt: attributes.desc,
date_published: attributes.pubdate,
tags: attributes.tags || [],
content: body,
};
output.push(entry);
}
console.log(JSON.stringify(output));