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]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"
}, }
} }

View file

@ -1,2 +1,2 @@
// eslint-disable-next-line import/no-anonymous-default-export // 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({ const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment // 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 // Add any custom config to be passed to Jest
const config = { const config = {
coverageProvider: 'v8', coverageProvider: "v8",
testEnvironment: 'jsdom' testEnvironment: "jsdom",
// Add more setup options before each test is run // Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'], // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
} };
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async // 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 fs = require("fs");
const fm = require('front-matter') const fm = require("front-matter");
/** @type {import('next-sitemap').IConfig} */ /** @type {import('next-sitemap').IConfig} */
module.exports = { module.exports = {
siteUrl: process.env.SITE_URL || 'https://www.aaronjy.me', siteUrl: process.env.SITE_URL || "https://www.aaronjy.me",
changefreq: 'weekly', changefreq: "weekly",
generateRobotsTxt: true, generateRobotsTxt: true,
autoLastmod: false, autoLastmod: false,
generateIndexSitemap: false, generateIndexSitemap: false,
robotsTxtOptions: { robotsTxtOptions: {
policies: [ policies: [
{ {
userAgent: '*', userAgent: "*",
allow: '/' allow: "/",
} },
] ],
} },
// transform: async (config, path) => { // transform: async (config, path) => {
// const metadata = { // const metadata = {
// loc: path // loc: path
@ -39,31 +39,31 @@ module.exports = {
// return metadata // return metadata
// } // }
};
function isHomepage(path) {
return path === "/";
} }
function isHomepage (path) { function isBasePage(path) {
return path === '/' return path.split("/").length === 2;
} }
function isBasePage (path) { function isArticle(path) {
return path.split('/').length === 2 return path.startsWith("/writing/");
} }
function isArticle (path) { function getArticleAttibutes(path) {
return path.startsWith('/writing/')
}
function getArticleAttibutes (path) {
const fileContents = fs.readFileSync(path, { const fileContents = fs.readFileSync(path, {
encoding: 'utf-8' encoding: "utf-8",
}) });
// @ts-ignore // @ts-ignore
const { attributes } = fm(fileContents) const { attributes } = fm(fileContents);
return { return {
...attributes, ...attributes,
pubdate: attributes.pubdate?.toUTCString() ?? null, 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} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone', output: "standalone",
images: { 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", "name": "www-aaronjy-me",
"version": "2.0.0.0", "version": "2.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "www-aaronjy-me", "name": "www-aaronjy-me",
"version": "2.0.0.0", "version": "2.1.1",
"dependencies": { "dependencies": {
"@highlightjs/cdn-assets": "^11.11.1", "@highlightjs/cdn-assets": "^11.11.1",
"@mdx-js/mdx": "^3.1.0", "@mdx-js/mdx": "^3.1.0",
@ -45,7 +45,9 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lint-staged": "^15.5.1",
"next-sitemap": "^4.0.9", "next-sitemap": "^4.0.9",
"prettier": "3.5.3",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"showdown": "^2.1.0" "showdown": "^2.1.0"
} }
@ -6234,6 +6236,93 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@ -6324,6 +6413,13 @@
"simple-swizzle": "^0.2.2" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -7013,6 +7109,19 @@
"node": ">=6" "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": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -8030,6 +8139,13 @@
"node": ">=0.10.0" "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": { "node_modules/execa": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@ -8360,6 +8476,19 @@
"node": "6.* || 8.* || >= 10.*" "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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -12525,6 +12654,19 @@
"node": ">= 0.8.0" "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": { "node_modules/lines-and-columns": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@ -12540,6 +12682,160 @@
"uc.micro": "^1.0.1" "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": { "node_modules/loader-utils": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
@ -12636,6 +12932,160 @@
"integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
"dev": true "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": { "node_modules/longest-streak": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@ -13017,6 +13467,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/min-indent": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -16093,6 +16556,19 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@ -16228,6 +16704,22 @@
"node": ">= 0.8.0" "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": { "node_modules/pretty-format": {
"version": "27.5.1", "version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
@ -18591,6 +19083,39 @@
"node": ">=10" "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": { "node_modules/reusify": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@ -18601,6 +19126,13 @@
"node": ">=0.10.0" "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": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -19019,6 +19551,49 @@
"node": ">=8" "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -19086,6 +19661,16 @@
"node": ">=10.0.0" "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": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,31 @@
import style from './BookReviewItem.module.css' import style from "./BookReviewItem.module.css";
import Link from 'next/link' import Link from "next/link";
export default function BookReviewItem ({ href, title, author, rating, image, tags }) { export default function BookReviewItem({
const imageUrl = image ? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}` : undefined href,
title,
author,
rating,
image,
tags,
}) {
const imageUrl = image
? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}`
: undefined;
return ( return (
<div className={style.item}> <div className={style.item}>
<Link href={href}> <Link href={href}>
<div <div
className={style.thumb} style={{ className={style.thumb}
backgroundImage: `url(${imageUrl ?? '/img/book-placeholder.jpg'})` 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> </div>
</Link> </Link>
<div> <div>
@ -19,19 +33,25 @@ export default function BookReviewItem ({ href, title, author, rating, image, ta
<Link href={href}>{title}</Link> <Link href={href}>{title}</Link>
</h2> </h2>
<p className={style.author}>{author}</p> <p className={style.author}>{author}</p>
{tags?.length && <p>{tags.join(', ')}</p>} {tags?.length && <p>{tags.join(", ")}</p>}
</div> </div>
</div> </div>
) );
} }
function Star () { function Star() {
return ( return (
<svg <svg
style={{ style={{
fill: 'currentColor' 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' /> 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> </svg>
) );
} }

View file

@ -15,7 +15,6 @@
/* font-weight: bold; */ /* font-weight: bold; */
} }
.thumb { .thumb {
width: auto; width: auto;
height: 290px; height: 290px;
@ -37,4 +36,4 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
pointer-events: none; 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. * Sets default values for external links on an anchor tag.
* @returns * @returns
*/ */
function ExternalLink ({ function ExternalLink({
href, href,
rel = 'nofollow noopener', rel = "nofollow noopener",
children, children,
target = '_blank' target = "_blank",
}) { }) {
return ( return (
<> <>
<a href={href} rel={rel} target={target}> <a href={href} rel={rel} target={target}>
<svg <svg
viewBox='0 0 24 24' viewBox="0 0 24 24"
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
style={{ display: 'inline', width: '1rem', marginRight: '0.25rem' }} style={{ display: "inline", width: "1rem", marginRight: "0.25rem" }}
> >
<g> <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> </g>
</svg> </svg>
{children} {children}
</a> </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 // @ts-ignore
import pck from '../../../package.json' import pck from "../../../package.json";
function Footer () { function Footer() {
return ( return (
<footer className={style.footer} data-testid='footer'> <footer className={style.footer} data-testid="footer">
<hr /> <hr />
<nav> <nav>
<div> <div>
<span> <span>
<a href='#'>Back to top</a> <a href="#">Back to top</a>
</span>{', '} </span>
{", "}
<span> <span>
<a href='/static/pgp.txt'>PGP key</a> <a href="/static/pgp.txt">PGP key</a>
</span>{', '} </span>
{", "}
<span> <span>
<a href='mailto:me@aaronjy.me'>Send me an email</a> <a href="mailto:me@aaronjy.me">Send me an email</a>
</span>{', '} </span>
{", "}
<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> </span>
</div> </div>
<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> </div>
</nav> </nav>
</footer> </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 }) { export default function Grid({ columns, children }) {
return ( return <div className={style.grid}>{children}</div>;
<div
className={style.grid}
>
{children}
</div>
)
} }

View file

@ -1,21 +1,26 @@
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} data-testid='header'> <header className={styles.header} data-testid="header">
<nav> <nav>
<Link href='/'>Home</Link>{', '} <Link href="/">Home</Link>
<Link href='/writing'>Writing</Link>{', '} {", "}
<Link href='/tags'>Tags</Link>{', '} <Link href="/writing">Writing</Link>
<Link href='/cv'>CV</Link>{', '} {", "}
<Link href='/library'>Library</Link>{', '} <Link href="/tags">Tags</Link>
<Link href='/about'>About</Link> {", "}
<Link href="/cv">CV</Link>
{", "}
<Link href="/library">Library</Link>
{", "}
<Link href="/about">About</Link>
</nav> </nav>
</header> </header>
) );
} }
export default Header export default Header;

View file

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

View file

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

View file

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

View file

@ -1,17 +1,17 @@
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 }) { function DefaultLayout({ children }) {
return ( return (
<main className={`${style.layout}`}> <main className={`${style.layout}`}>
<Header /> <Header />
<>{children}</> <>{children}</>
<Footer /> <Footer />
</main> </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) { export function filenameToSlug(input) {
return stringToSlug(input.substring(0, input.indexOf('.'))) return stringToSlug(input.substring(0, input.indexOf(".")));
} }
export function stringToSlug (str) { export function stringToSlug(str) {
return str return str
.trim() .trim()
.toLowerCase() .toLowerCase()
.replace(/[\W_]+/g, '-') .replace(/[\W_]+/g, "-")
.replace(/^-+|-+$/g, '') .replace(/^-+|-+$/g, "");
}; }
export function formatDate (date) { export function formatDate(date) {
return dateFns.format(Date.parse(date), 'PPP') return dateFns.format(Date.parse(date), "PPP");
} }
/** /**
@ -22,6 +22,6 @@ export function formatDate (date) {
* @param {*} obj * @param {*} obj
* @returns * @returns
*/ */
export function stringifyAndParse (obj) { export function stringifyAndParse(obj) {
return JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj));
} }

View file

@ -1,37 +1,44 @@
import StaticContentList from '@/components/StaticContentList/StaticContentList' import StaticContentList from "@/components/StaticContentList/StaticContentList";
import { fetchPosts } from '@/services/content-service' import { fetchPosts } from "@/services/content-service";
import { useEffect, useState, useTransition } from 'react' import { useEffect, useState, useTransition } from "react";
export const mdxComponents = { export const mdxComponents = {
StaticContentList: ({ type, urlPrefix, max = 0 }) => { StaticContentList: ({ type, urlPrefix, max = 0 }) => {
const [items, setItems] = useState([]) const [items, setItems] = useState([]);
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false);
useEffect(function () { useEffect(
if (!urlPrefix || max <= 0) function () {
return; if (!urlPrefix || max <= 0) return;
switch (type) { switch (type) {
case 'posts': case "posts":
(async function () { (async function () {
setLoading(true) setLoading(true);
const res = await fetchPosts([]) const res = await fetchPosts([]);
const json = await res.json() const json = await res.json();
setItems(json.data.sort((a, b) => a.datePublished < b.datePublished) ?? []) setItems(
setLoading(false) json.data.sort((a, b) => a.datePublished < b.datePublished) ??
})() [],
break; );
setLoading(false);
})();
break;
default: default:
throw `Could not render StaticContentList: content type ${type} not supported.` throw `Could not render StaticContentList: content type ${type} not supported.`;
} }
}, [type, urlPrefix, max]) },
[type, urlPrefix, max],
);
return ( return (
<> <>
{isLoading && <p>Loading...</p> } {isLoading && <p>Loading...</p>}
{!isLoading && <StaticContentList entries={items} urlPrefix={urlPrefix} max={max} />} {!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 { import {
serialize FailedFetchBasicPageError,
} from 'next-mdx-remote-client/serialize' FailedFetchBasicPagesError,
import { NextSeo } from 'next-seo' } 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 }) { export async function getServerSideProps({ params }) {
const { path } = params const { path } = params;
console.log(params) console.log(params);
const res = await fetchBasicPages([], { const res = await fetchBasicPages([], {
path: { path: {
_eq: path?.join('/') ?? null _eq: path?.join("/") ?? null,
} },
}) });
if (!res.ok) { 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) { if (!page) {
return { return {
notFound: true notFound: true,
} };
} }
const { content, title } = page const { content, title } = page;
const mdxSource = await serialize({ source: content }) const mdxSource = await serialize({ source: content });
return { return {
props: { props: {
title, title,
mdxSource mdxSource,
} },
} };
} }
export default function BasicPage ({ title, mdxSource }) { export default function BasicPage({ title, mdxSource }) {
if (!mdxSource || 'error' in mdxSource) { if (!mdxSource || "error" in mdxSource) {
return <p>Something went wrong: {mdxSource?.error ?? '???'}</p> return <p>Something went wrong: {mdxSource?.error ?? "???"}</p>;
} }
return ( return (
<DefaultLayout> <DefaultLayout>
<NextSeo <NextSeo
title={title} description={undefined} openGraph={ title={title}
{ description={undefined}
title, openGraph={{
description: undefined title,
} description: undefined,
} }}
/> />
<div> <div>
<MDXClient {...mdxSource} components={mdxComponents} /> <MDXClient {...mdxSource} components={mdxComponents} />
</div> </div>
</DefaultLayout> </DefaultLayout>
) );
} }

View file

@ -1,12 +1,14 @@
import { DefaultSeo } from 'next-seo' import { DefaultSeo } from "next-seo";
import '@/styles/globals.css' import "@/styles/globals.css";
export default function App ({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return ( return (
<> <>
<DefaultSeo defaultTitle='Aaron Yarborough' titleTemplate='%s | Aaron Yarborough' /> <DefaultSeo
<Component defaultTitle="Aaron Yarborough"
{...pageProps} /> 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 ( return (
<Html lang='en'> <Html lang="en">
<Head> <Head>
<link rel='stylesheet' href='https://neat.joeldare.com/neat.css' /> <link rel="stylesheet" href="https://neat.joeldare.com/neat.css" />
<script defer data-domain='aaronjy.me' src='https://analytics.aaronjy.me/js/script.js' /> <script
defer
data-domain="aaronjy.me"
src="https://analytics.aaronjy.me/js/script.js"
/>
</Head> </Head>
<body> <body>
<Main /> <Main />
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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