Compare commits
No commits in common. "main" and "add-decap" have entirely different histories.
72 changed files with 2707 additions and 19280 deletions
|
@ -1,7 +0,0 @@
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
README.md
|
|
||||||
.next
|
|
||||||
.git
|
|
|
@ -1,2 +0,0 @@
|
||||||
# NEXT_PUBLIC_CONTENT_API_BASE_URL=http://localhost:8055
|
|
||||||
NEXT_PUBLIC_CONTENT_API_BASE_URL=https://cms.aaronjy.me
|
|
|
@ -1,3 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals"]
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"@stylistic/jsx/jsx-pascal-case": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -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
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
npx --no -- commitlint --edit $1
|
|
|
@ -1 +1,3 @@
|
||||||
npx lint-staged
|
echo Formatting...
|
||||||
|
npm run format
|
||||||
|
echo Successfully formatted.
|
1
.nvmrc
1
.nvmrc
|
@ -1 +0,0 @@
|
||||||
v22.15.0
|
|
47
.vscode/settings.json
vendored
47
.vscode/settings.json
vendored
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
71
Dockerfile
71
Dockerfile
|
@ -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"]
|
|
|
@ -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
14
content/home.md
Normal 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
163
content/pages/cv.yml
Normal 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
|
16
content/recipes/manakish-pan.md
Normal file
16
content/recipes/manakish-pan.md
Normal 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)
|
10
deploy.sh
10
deploy.sh
|
@ -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!"
|
|
|
@ -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);
|
|
|
@ -2,8 +2,6 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
"checkJs": true,
|
|
||||||
"jsx": "preserve"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/");
|
|
||||||
}
|
|
|
@ -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
19510
package-lock.json
generated
File diff suppressed because it is too large
Load diff
61
package.json
61
package.json
|
@ -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
59
public/admin/config.yml
Normal 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
13
public/admin/index.html
Normal 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
1
public/next.svg
Normal 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 |
|
@ -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
|
|
|
@ -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>
|
|
|
@ -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
1
public/vercel.svg
Normal 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 |
|
@ -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;
|
|
|
@ -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> {tags.join(",")}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span className="bold">Rating:</span> {rating}/5
|
|
||||||
<br />
|
|
||||||
<span className="bold">Read on:</span>
|
|
||||||
{formatDate(readDate)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<ExternalLink href={url}>View on The StoryGraph</ExternalLink>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BookReview;
|
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 />
|
|
||||||
{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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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
56
src/components/CV/CV.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
29
src/components/CV/CV.module.css
Normal file
29
src/components/CV/CV.module.css
Normal 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;
|
||||||
|
}
|
31
src/components/ExternalLink.jsx
Normal file
31
src/components/ExternalLink.jsx
Normal 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
|
|
@ -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;
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.footer nav:first-child a {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.header {
|
.header nav {
|
||||||
margin-top: 20px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header a {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
export default function Loading() {
|
|
||||||
return <p>Loading...</p>;
|
|
||||||
}
|
|
|
@ -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 />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -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} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
76
src/pages/index.js
Normal 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'm Aaron. I'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've always had a habit of
|
||||||
|
'lurking' online; I barely interact with the content I
|
||||||
|
consume, and you'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 "Goodreads for film."
|
||||||
|
</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'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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
46
src/pages/recipes/index.js
Normal file
46
src/pages/recipes/index.js
Normal 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 }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -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() {}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
|
|
@ -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));
|
|
Loading…
Add table
Reference in a new issue