refactor: auto format

This commit is contained in:
Aaron Yarborough 2025-05-04 15:22:06 +01:00
parent 8524db8ecf
commit 895542494a
38 changed files with 1254 additions and 565 deletions

View file

@ -1,3 +1,3 @@
{
"extends": ["next/core-web-vitals"]
"extends": ["next/core-web-vitals"]
}

View file

@ -1 +1 @@
npm run lint
npx lint-staged

View file

@ -46,5 +46,5 @@
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
}
}
}

View file

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

View file

@ -1,17 +1,17 @@
import nextJest from 'next/jest.js'
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: './'
})
dir: "./",
});
// Add any custom config to be passed to Jest
const config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom'
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)
export default createJestConfig(config);

View file

@ -1,21 +1,21 @@
const fs = require('fs')
const fm = require('front-matter')
const fs = require("fs");
const fm = require("front-matter");
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || 'https://www.aaronjy.me',
changefreq: 'weekly',
siteUrl: process.env.SITE_URL || "https://www.aaronjy.me",
changefreq: "weekly",
generateRobotsTxt: true,
autoLastmod: false,
generateIndexSitemap: false,
robotsTxtOptions: {
policies: [
{
userAgent: '*',
allow: '/'
}
]
}
userAgent: "*",
allow: "/",
},
],
},
// transform: async (config, path) => {
// const metadata = {
// loc: path
@ -39,31 +39,31 @@ module.exports = {
// return metadata
// }
};
function isHomepage(path) {
return path === "/";
}
function isHomepage (path) {
return path === '/'
function isBasePage(path) {
return path.split("/").length === 2;
}
function isBasePage (path) {
return path.split('/').length === 2
function isArticle(path) {
return path.startsWith("/writing/");
}
function isArticle (path) {
return path.startsWith('/writing/')
}
function getArticleAttibutes (path) {
function getArticleAttibutes(path) {
const fileContents = fs.readFileSync(path, {
encoding: 'utf-8'
})
encoding: "utf-8",
});
// @ts-ignore
const { attributes } = fm(fileContents)
const { attributes } = fm(fileContents);
return {
...attributes,
pubdate: attributes.pubdate?.toUTCString() ?? null,
moddate: attributes.moddate?.toUTCString() ?? null
}
moddate: attributes.moddate?.toUTCString() ?? null,
};
}

View file

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

589
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "www-aaronjy-me",
"version": "2.0.0.0",
"version": "2.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "www-aaronjy-me",
"version": "2.0.0.0",
"version": "2.1.1",
"dependencies": {
"@highlightjs/cdn-assets": "^11.11.1",
"@mdx-js/mdx": "^3.1.0",
@ -45,7 +45,9 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"js-yaml": "^4.1.0",
"lint-staged": "^15.5.1",
"next-sitemap": "^4.0.9",
"prettier": "3.5.3",
"react-test-renderer": "^18.3.1",
"showdown": "^2.1.0"
}
@ -6234,6 +6236,93 @@
"dev": true,
"license": "MIT"
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cli-truncate/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@ -6324,6 +6413,13 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -7013,6 +7109,19 @@
"node": ">=6"
}
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -8030,6 +8139,13 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"dev": true,
"license": "MIT"
},
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@ -8360,6 +8476,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-east-asian-width": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
"integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -12525,6 +12654,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -12540,6 +12682,160 @@
"uc.micro": "^1.0.1"
}
},
"node_modules/lint-staged": {
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz",
"integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"commander": "^13.1.0",
"debug": "^4.4.0",
"execa": "^8.0.1",
"lilconfig": "^3.1.3",
"listr2": "^8.2.5",
"micromatch": "^4.0.8",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.7.0"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=18.12.0"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/lint-staged/node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/listr2": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz",
"integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/listr2/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/listr2/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/listr2/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/listr2/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
@ -12636,6 +12932,160 @@
"integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
"dev": true
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-escapes": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
"integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/log-update/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
"dev": true,
"license": "MIT"
},
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@ -13017,6 +13467,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -16093,6 +16556,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidtree": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
"dev": true,
"license": "MIT",
"bin": {
"pidtree": "bin/pidtree.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/pirates": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@ -16228,6 +16704,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@ -18591,6 +19083,39 @@
"node": ">=10"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/restore-cursor/node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@ -18601,6 +19126,13 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -19019,6 +19551,49 @@
"node": ">=8"
}
},
"node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -19086,6 +19661,16 @@
"node": ">=10.0.0"
}
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",

View file

@ -3,12 +3,16 @@
"version": "2.1.1",
"private": true,
"type": "module",
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"scripts": {
"dev": "next dev",
"build": "next build",
"postbuild": "next-sitemap --config next-sitemap.config.cjs",
"start": "next start",
"link": "echo NOT CONFIGURED",
"format": "prettier . --write",
"prepare": "husky",
"test": "jest --verbose --passWithNoTests",
"lint": "next lint",
@ -53,7 +57,9 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"js-yaml": "^4.1.0",
"lint-staged": "^15.5.1",
"next-sitemap": "^4.0.9",
"prettier": "3.5.3",
"react-test-renderer": "^18.3.1",
"showdown": "^2.1.0"
}

View file

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

View file

@ -1,56 +1,81 @@
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'
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
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
const imageUrl = image
? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}`
: undefined;
return (
<>
<h1>{title}<br /><small>by {author}</small></h1>
<Link href='./'>Back...</Link>
<h1>
{title}
<br />
<small>by {author}</small>
</h1>
<Link href="./">Back...</Link>
<article>
<NextSeo
title={title} description={description} openGraph={
{
title,
description,
type: 'article',
article: {
publishedTime: readDate ?? null
}
}
}
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} />}
{imageUrl && (
<Image
src={imageUrl}
width={250}
height={580}
alt=""
className={style.thumbnail}
/>
)}
<div>
<div data-test='content' dangerouslySetInnerHTML={{ __html: html || '<p>(no review)</p>' }} />
<div
data-test="content"
dangerouslySetInnerHTML={{
__html: html || "<p>(no review)</p>",
}}
/>
<p>
{tags?.length && <>
<span className='bold'>Genres:</span>&nbsp;{tags.join(',')}<br />
</>}
<span className='bold'>Rating:</span>&nbsp;{rating}/5<br />
<span className='bold'>Read on:</span>&nbsp;{formatDate(readDate)}
{tags?.length && (
<>
<span className="bold">Genres:</span>&nbsp;{tags.join(",")}
<br />
</>
)}
<span className="bold">Rating:</span>&nbsp;{rating}/5
<br />
<span className="bold">Read on:</span>&nbsp;
{formatDate(readDate)}
</p>
<p>
<ExternalLink href={url}>View on The StoryGraph</ExternalLink>
</p>
<p><ExternalLink href={url}>View on The StoryGraph</ExternalLink></p>
</div>
</div>
</div>
</article>
</>
)
);
}
export default BookReview
export default BookReview;

View file

@ -1,4 +1,3 @@
.layout {
display: flex;
flex-direction: row;
@ -31,4 +30,4 @@
.thumbnail {
width: 100%;
}
}
}

View file

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

View file

@ -15,7 +15,6 @@
/* font-weight: bold; */
}
.thumb {
width: auto;
height: 290px;
@ -37,4 +36,4 @@
flex-direction: row;
align-items: center;
pointer-events: none;
}
}

View file

@ -1,31 +1,34 @@
import React from 'react'
import React from "react";
/**
* Sets default values for external links on an anchor tag.
* @returns
*/
function ExternalLink ({
function ExternalLink({
href,
rel = 'nofollow noopener',
rel = "nofollow noopener",
children,
target = '_blank'
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' }}
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' />
<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
export default ExternalLink;

View file

@ -1,37 +1,48 @@
import React from 'react'
import React from "react";
import style from './Footer.module.css'
import style from "./Footer.module.css";
// @ts-ignore
import pck from '../../../package.json'
import pck from "../../../package.json";
function Footer () {
function Footer() {
return (
<footer className={style.footer} data-testid='footer'>
<footer className={style.footer} data-testid="footer">
<hr />
<nav>
<div>
<span>
<a href='#'>Back to top</a>
</span>{', '}
<a href="#">Back to top</a>
</span>
{", "}
<span>
<a href='/static/pgp.txt'>PGP key</a>
</span>{', '}
<a href="/static/pgp.txt">PGP key</a>
</span>
{", "}
<span>
<a href='mailto:me@aaronjy.me'>Send me an email</a>
</span>{', '}
<a href="mailto:me@aaronjy.me">Send me an email</a>
</span>
{", "}
<span>
<a href='https://git.aaronjy.me/aaron/aaronjy-me' target='_blank' rel='nofollow noopener noreferrer'>View site src</a>
<a
href="https://git.aaronjy.me/aaron/aaronjy-me"
target="_blank"
rel="nofollow noopener noreferrer"
>
View site src
</a>
</span>
</div>
<div>
<small>2025 Aaron Yarborough, <span title='major.minior.patch.content'>v{pck.version}</span></small>
<small>
2025 Aaron Yarborough,{" "}
<span title="major.minior.patch.content">v{pck.version}</span>
</small>
</div>
</nav>
</footer>
)
);
}
export default Footer
export default Footer;

View file

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

View file

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

View file

@ -1,3 +1,3 @@
.header {
margin-top: 20px;
}
margin-top: 20px;
}

View file

@ -1,26 +1,36 @@
import React from 'react'
import React from "react";
import style from './Resume.module.css'
import { markdownToHtml } from '@/services/content-service'
import style from "./Resume.module.css";
import { markdownToHtml } from "@/services/content-service";
function Resume ({
function Resume({
competencies,
education,
certifications,
languages,
experience
experience,
}) {
return (
<div className={style.cv}>
<ol>
<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>
<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>
<h2 id='experience'>Professional experience</h2>
<h2 id="experience">Professional experience</h2>
{experience?.map((exp, i) => (
<div key={i}>
@ -32,30 +42,35 @@ function Resume ({
>
{markdownToHtml(exp.description)}
</WorkExperience>
{!!exp.skills?.length && <details>
<summary>Competencies</summary>
<>{exp.skills.sort().join(', ')}</>
</details>}
{!!exp.skills?.length && (
<details>
<summary>Competencies</summary>
<>{exp.skills.sort().join(", ")}</>
</details>
)}
</div>
))}
</div>
<div className='sidebar'>
<h2 id='competencies'>Competencies</h2>
<div className="sidebar">
<h2 id="competencies">Competencies</h2>
<ul>
{competencies?.sort((a, b) => a.name > b.name).map((c, i) => (
<li key={i}>{c.name}</li>
))}
{competencies
?.sort((a, b) => a.name > b.name)
.map((c, i) => (
<li key={i}>{c.name}</li>
))}
</ul>
<h2 id='certifications'>Certifications</h2>
<h2 id="certifications">Certifications</h2>
<ul>
{certifications?.sort((a, b) => a.name > b.name).map((c, i) => (
<li key={i}>{c.name}</li>
))}
{certifications
?.sort((a, b) => a.name > b.name)
.map((c, i) => (
<li key={i}>{c.name}</li>
))}
</ul>
<h2 className='languages'>Languages</h2>
<h2 className="languages">Languages</h2>
<ul>
{languages?.sort().map((c, i) => (
<li key={i}>
@ -64,19 +79,18 @@ function Resume ({
))}
</ul>
<h2 className='education'>Education</h2>
<h2 className="education">Education</h2>
<p>{education.name}</p>
</div>
</div>
)
);
}
export default Resume
export default Resume;
function WorkExperience ({ position, employer, start, end, children }) {
function WorkExperience({ position, employer, start, end, children }) {
return (
<div className={style['work-experience']}>
<div className={style["work-experience"]}>
<div>
<h3 id={position}>
{position}
@ -88,9 +102,9 @@ function WorkExperience ({ position, employer, start, end, children }) {
</small>
</div>
<div
data-test='children'
data-test="children"
dangerouslySetInnerHTML={{ __html: children }}
/>
</div>
)
);
}

View file

@ -1,19 +1,25 @@
import { formatDate } from '@/lib/helpers'
import Link from 'next/link'
import { formatDate } from "@/lib/helpers";
import Link from "next/link";
export default function StaticContentList ({ entries, urlPrefix, max = 0 }) {
export default function StaticContentList({ entries, urlPrefix, max = 0 }) {
return (
<table>
<tbody>
{entries.map((e) => (
<tr key={e.slug}>
<td>{!!e.datePublished && <span>{formatDate(e.datePublished)}</span>}</td>
<td>
<Link href={`${urlPrefix}${e.slug}`}>{e.title}</Link>
</td>
</tr>
)).slice(0, max > 0 ? max : entries.length)}
{entries
.map((e) => (
<tr key={e.slug}>
<td>
{!!e.datePublished && (
<span>{formatDate(e.datePublished)}</span>
)}
</td>
<td>
<Link href={`${urlPrefix}${e.slug}`}>{e.title}</Link>
</td>
</tr>
))
.slice(0, max > 0 ? max : entries.length)}
</tbody>
</table>
)
);
}

View file

@ -1,56 +1,56 @@
// Posts
export class FailedFetchPostsError extends Error {
constructor (msg) {
super(`Failed to fetch posts: ${msg}`)
this.name = 'FailedFetchPostsError'
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'
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'
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'
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'
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'
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'
constructor(msg) {
super(`Failed to fetch basic pages: ${msg}`);
this.name = "FailedFetchCVError";
}
}

View file

@ -1,17 +1,17 @@
import React from 'react'
import React from "react";
import style from './DefaultLayout.module.css'
import Header from '@/components/Header/Header'
import Footer from '@/components/Footer/Footer'
import style from "./DefaultLayout.module.css";
import Header from "@/components/Header/Header";
import Footer from "@/components/Footer/Footer";
function DefaultLayout ({ children }) {
function DefaultLayout({ children }) {
return (
<main className={`${style.layout}`}>
<Header />
<>{children}</>
<Footer />
<Footer />
</main>
)
);
}
export default DefaultLayout
export default DefaultLayout;

View file

@ -1,19 +1,19 @@
import * as dateFns from 'date-fns'
import * as dateFns from "date-fns";
export function filenameToSlug (input) {
return stringToSlug(input.substring(0, input.indexOf('.')))
export function filenameToSlug(input) {
return stringToSlug(input.substring(0, input.indexOf(".")));
}
export function stringToSlug (str) {
export function stringToSlug(str) {
return str
.trim()
.toLowerCase()
.replace(/[\W_]+/g, '-')
.replace(/^-+|-+$/g, '')
};
.replace(/[\W_]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function formatDate (date) {
return dateFns.format(Date.parse(date), 'PPP')
export function formatDate(date) {
return dateFns.format(Date.parse(date), "PPP");
}
/**
@ -22,6 +22,6 @@ export function formatDate (date) {
* @param {*} obj
* @returns
*/
export function stringifyAndParse (obj) {
return JSON.parse(JSON.stringify(obj))
export function stringifyAndParse(obj) {
return JSON.parse(JSON.stringify(obj));
}

View file

@ -1,37 +1,44 @@
import StaticContentList from '@/components/StaticContentList/StaticContentList'
import { fetchPosts } from '@/services/content-service'
import { useEffect, useState, useTransition } from 'react'
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)
const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(false);
useEffect(function () {
if (!urlPrefix || max <= 0)
return;
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;
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])
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} />}
{isLoading && <p>Loading...</p>}
{!isLoading && (
<StaticContentList entries={items} urlPrefix={urlPrefix} max={max} />
)}
</>
)
}
}
);
},
};

View file

@ -1,63 +1,64 @@
import { FailedFetchBasicPageError, 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'
FailedFetchBasicPageError,
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";
export async function getServerSideProps ({ params }) {
const { path } = params
console.log(params)
export async function getServerSideProps({ params }) {
const { path } = params;
console.log(params);
const res = await fetchBasicPages([], {
path: {
_eq: path?.join('/') ?? null
}
})
_eq: path?.join("/") ?? null,
},
});
if (!res.ok) {
throw new FailedFetchBasicPageError(path, await res.text())
throw new FailedFetchBasicPageError(path, await res.text());
}
const page = (await res.json()).data.at(0)
const page = (await res.json()).data.at(0);
if (!page) {
return {
notFound: true
}
notFound: true,
};
}
const { content, title } = page
const mdxSource = await serialize({ source: content })
const { content, title } = page;
const mdxSource = await serialize({ source: content });
return {
props: {
title,
mdxSource
}
}
mdxSource,
},
};
}
export default function BasicPage ({ title, mdxSource }) {
if (!mdxSource || 'error' in mdxSource) {
return <p>Something went wrong: {mdxSource?.error ?? '???'}</p>
export default function BasicPage({ title, mdxSource }) {
if (!mdxSource || "error" in mdxSource) {
return <p>Something went wrong: {mdxSource?.error ?? "???"}</p>;
}
return (
<DefaultLayout>
<NextSeo
title={title} description={undefined} openGraph={
{
title,
description: undefined
}
}
title={title}
description={undefined}
openGraph={{
title,
description: undefined,
}}
/>
<div>
<MDXClient {...mdxSource} components={mdxComponents} />
</div>
</DefaultLayout>
)
);
}

View file

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

View file

@ -1,16 +1,20 @@
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 (
<Html lang='en'>
<Html lang="en">
<Head>
<link rel='stylesheet' href='https://neat.joeldare.com/neat.css' />
<script defer data-domain='aaronjy.me' src='https://analytics.aaronjy.me/js/script.js' />
<link rel="stylesheet" href="https://neat.joeldare.com/neat.css" />
<script
defer
data-domain="aaronjy.me"
src="https://analytics.aaronjy.me/js/script.js"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
);
}

View file

@ -1,27 +1,27 @@
import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
import React from 'react'
import { NextSeo } from 'next-seo'
import Resume from '@/components/Resume/Resume'
import { fetchCV } from '@/services/content-service'
import { FailedFetchCVError } from '@/errors'
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout";
import React from "react";
import { NextSeo } from "next-seo";
import Resume from "@/components/Resume/Resume";
import { fetchCV } from "@/services/content-service";
import { FailedFetchCVError } from "@/errors";
export const Title = 'CV'
export const Title = "CV";
export async function getServerSideProps () {
const res = await fetchCV([])
export async function getServerSideProps() {
const res = await fetchCV([]);
if (!res.ok) {
throw new FailedFetchCVError(await res.text())
throw new FailedFetchCVError(await res.text());
}
const cv = (await res.json()).data
const cv = (await res.json()).data;
if (!cv) {
return {
notFound: true
}
notFound: true,
};
}
const { competencies, education, languages, certifications, experience } = cv
const { competencies, education, languages, certifications, experience } = cv;
return {
props: {
@ -29,26 +29,25 @@ export async function getServerSideProps () {
education,
languages,
certifications,
experience
}
}
experience,
},
};
}
export default function ResumePage ({
export default function ResumePage({
competencies,
education,
certifications,
languages,
experience
experience,
}) {
return (
<DefaultLayout>
<NextSeo
title={Title} openGraph={
{
title: Title
}
}
title={Title}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
<section>
@ -61,5 +60,5 @@ export default function ResumePage ({
/>
</section>
</DefaultLayout>
)
);
}

View file

@ -1,8 +1,11 @@
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 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";
// export async function getStaticPaths () {
// const res = await fetchBookReviews(['slug'], {
@ -26,45 +29,45 @@ import { FailedFetchBookReviewError, FailedFetchBookReviewsError } from '@/error
// }
export const getServerSideProps = async ({ params }) => {
const { slug } = params
const { slug } = params;
if (!slug) {
return {
notFound: true
}
notFound: true,
};
}
const res = await fetchBookReviews([], {
slug,
status: 'published'
})
status: "published",
});
if (!res.ok) {
throw new FailedFetchBookReviewError(slug, await res.text())
throw new FailedFetchBookReviewError(slug, await res.text());
}
const review = (await res.json()).data.at(0)
const review = (await res.json()).data.at(0);
if (!review) {
return {
notFound: true
}
notFound: true,
};
}
const content = review.review
const html = markdownToHtml(content)
const content = review.review;
const html = markdownToHtml(content);
return {
props: {
review,
html
}
}
}
html,
},
};
};
export default function LibrarySingle ({ review, html }) {
export default function LibrarySingle({ review, html }) {
return (
<DefaultLayout>
<BookReview review={review} html={html} />
</DefaultLayout>
)
);
}

View file

@ -1,40 +1,39 @@
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'
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 const Title = "Library";
export async function getServerSideProps () {
const res = await fetchBookReviews()
export async function getServerSideProps() {
const res = await fetchBookReviews();
if (!res.ok) {
throw new FailedFetchBookReviewsError(await res.text())
throw new FailedFetchBookReviewsError(await res.text());
}
const reviews = (await res.json()).data
.sort((a, b) => new Date(b.read_date).getTime() - new Date(a.read_date).getTime())
const reviews = (await res.json()).data.sort(
(a, b) => new Date(b.read_date).getTime() - new Date(a.read_date).getTime(),
);
return {
props: {
reviews: stringifyAndParse(reviews)
}
}
reviews: stringifyAndParse(reviews),
},
};
}
export default function Library ({ reviews }) {
export default function Library({ reviews }) {
return (
<DefaultLayout>
<NextSeo
title={Title}
openGraph={
{
title: Title
}
}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
@ -42,10 +41,14 @@ export default function Library ({ reviews }) {
<section>
<Grid columns={5}>
{reviews.map((review, _) => (
<BookReviewItem key={review.title} {...review} href={`/library/${review.slug}`} />
<BookReviewItem
key={review.title}
{...review}
href={`/library/${review.slug}`}
/>
))}
</Grid>
</section>
</DefaultLayout>
)
);
}

View file

@ -1,69 +1,72 @@
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'
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 getServerSideProps () {
const res = await fetchPosts(['title', 'date_published', 'tags', 'slug'], {
status: 'published'
})
export async function getServerSideProps() {
const res = await fetchPosts(["title", "date_published", "tags", "slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostsError(await res.text())
throw new FailedFetchPostsError(await res.text());
}
const posts = (await res.json()).data
const tags = getTagsFromPosts(posts)
const posts = (await res.json()).data;
const tags = getTagsFromPosts(posts);
return {
props: {
tags,
posts
}
}
posts,
},
};
}
export const Title = 'Tags'
export const Title = "Tags";
export default function Tags ({ tags, posts }) {
export default function Tags({ tags, posts }) {
return (
<DefaultLayout>
<NextSeo
title={Title}
openGraph={
{
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)
{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>
)
})}
return (
<React.Fragment key={tag}>
<h2>{tag}</h2>
<ul>
{tagPosts.map((post, _) => {
return (
<li key={post.slug}>
<Link href={`/writing/${post.slug}`}>{post.title}</Link>
</li>
);
})}
</ul>
</React.Fragment>
);
})}
</section>
</DefaultLayout>
)
);
}

View file

@ -1,8 +1,8 @@
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 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";
// export async function getStaticPaths () {
// const res = await fetchPosts(['slug'], {
@ -26,38 +26,38 @@ import { FailedFetchPostError, FailedFetchPostsError } from '@/errors'
// }
export const getServerSideProps = async ({ params }) => {
const { slug } = params
const { slug } = params;
const res = await fetchPosts([], {
slug,
status: 'published'
})
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostError(slug, await res.text())
throw new FailedFetchPostError(slug, await res.text());
}
const post = (await res.json()).data.at(0)
const post = (await res.json()).data.at(0);
if (!post) {
return {
notFound: true
}
notFound: true,
};
}
const { content } = post
const html = markdownToHtml(content)
const { content } = post;
const html = markdownToHtml(content);
return {
props: {
post,
html
}
}
}
html,
},
};
};
export default function WritingSingle ({ post, html }) {
export default function WritingSingle({ post, html }) {
return (
<DefaultLayout>
<Article {...post} html={html} />
</DefaultLayout>
)
);
}

View file

@ -1,48 +1,48 @@
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'
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 getServerSideProps = async () => {
const res = await fetchPosts(['title', 'date_published', 'slug'], {
status: 'published'
})
const res = await fetchPosts(["title", "date_published", "slug"], {
status: "published",
});
if (!res.ok) {
throw new FailedFetchPostsError(await res.text())
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())
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
}
}
}
posts,
},
};
};
export const Title = 'Writing'
export const Title = "Writing";
export default function Writing ({ posts }) {
export default function Writing({ posts }) {
return (
<DefaultLayout>
<NextSeo
title={Title}
openGraph={
{
title: Title
}
}
openGraph={{
title: Title,
}}
/>
<h1>{Title}</h1>
<section>
<StaticContentList entries={posts} urlPrefix='writing/' />
<StaticContentList entries={posts} urlPrefix="writing/" />
</section>
</DefaultLayout>
)
);
}

View file

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

View file

@ -1,24 +1,24 @@
pre {
padding: 0;
border-radius: 5px;
padding: 0;
border-radius: 5px;
}
tbody {
vertical-align: top;
vertical-align: top;
}
tr {
text-align: left;
text-align: left;
}
td:first-child {
padding-right: 20px;
padding-right: 20px;
}
.form-group {
margin: 20px 0;
margin: 20px 0;
}
.bold {
font-weight: 700;
}
font-weight: 700;
}

View file

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

View file

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