Compare commits

..

No commits in common. "main" and "standalone" have entirely different histories.

39 changed files with 621 additions and 1251 deletions

View file

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

10
.vscode/settings.json vendored
View file

@ -26,21 +26,25 @@
"Yarbz" "Yarbz"
], ],
"files.autoSave": "off", "files.autoSave": "off",
"editor.defaultFormatter": "esbenp.prettier-vscode", "standard.autoFixOnSave": true,
"editor.formatOnSave": true, "editor.defaultFormatter": "standard.vscode-standard",
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[javascriptreact]": { "[javascriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"liveServer.settings.multiRootWorkspaceName": "www-aaronjy-2024",
"[css]": { "[css]": {
"editor.defaultFormatter": "vscode.css-language-features" "editor.defaultFormatter": "vscode.css-language-features"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"
}, },
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"
} },
} }

View file

@ -1,4 +1,4 @@
FROM node:22-alpine AS base FROM node:18-alpine AS base
# Install dependencies only when needed # Install dependencies only when needed
FROM base AS deps FROM base AS deps

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 isBasePage(path) { function isHomepage (path) {
return path.split("/").length === 2; return path === '/'
} }
function isArticle(path) { function isBasePage (path) {
return path.startsWith("/writing/"); return path.split('/').length === 2
} }
function getArticleAttibutes(path) { function isArticle (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.1.1", "version": "2.0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "www-aaronjy-me", "name": "www-aaronjy-me",
"version": "2.1.1", "version": "2.0.0.0",
"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,9 +45,7 @@
"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"
} }
@ -6236,93 +6234,6 @@
"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",
@ -6413,13 +6324,6 @@
"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",
@ -7109,19 +7013,6 @@
"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",
@ -8139,13 +8030,6 @@
"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",
@ -8476,19 +8360,6 @@
"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",
@ -12654,19 +12525,6 @@
"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",
@ -12682,160 +12540,6 @@
"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",
@ -12932,160 +12636,6 @@
"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",
@ -13467,19 +13017,6 @@
"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",
@ -16556,19 +16093,6 @@
"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",
@ -16704,22 +16228,6 @@
"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",
@ -19083,39 +18591,6 @@
"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",
@ -19126,13 +18601,6 @@
"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",
@ -19551,49 +19019,6 @@
"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",
@ -19661,16 +19086,6 @@
"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

@ -1,18 +1,14 @@
{ {
"name": "www-aaronjy-me", "name": "www-aaronjy-me",
"version": "2.1.2", "version": "2.1.0",
"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",
@ -57,9 +53,7 @@
"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,40 +1,41 @@
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} title={title} description={excerpt} openGraph={
description={excerpt} {
openGraph={{ title,
title, description: excerpt,
description: excerpt, type: 'article',
type: "article", article: {
article: { publishedTime: datePublished ?? null
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,80 +1,56 @@
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 } = const { title, image, author, description, url, tags, rating, readDate } = review
review;
const imageUrl = image const imageUrl = image ? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}` : undefined
? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}`
: undefined;
return ( return (
<> <>
<h1> <h1>{title}<br /><small>by {author}</small></h1>
{title} <small>- {author}</small> <Link href='./'>Back...</Link>
</h1>
<Link href="./">Back...</Link>
<article> <article>
<NextSeo <NextSeo
title={title} title={title} description={description} openGraph={
description={description} {
openGraph={{ title,
title, description,
description, type: 'article',
type: "article", article: {
article: { publishedTime: readDate ?? null
publishedTime: readDate ?? null, }
}, }
}} }
/> />
<div> <div>
<div className={style.layout}> <div className={style.layout}>
{imageUrl && ( {imageUrl &&
<Image <Image src={imageUrl} width={250} height={580} alt='' className={style.thumbnail} />}
src={imageUrl}
width={250}
height={580}
alt=""
className={style.thumbnail}
/>
)}
<div> <div>
<div <div data-test='content' dangerouslySetInnerHTML={{ __html: html || '<p>(no review)</p>' }} />
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(",")} </>}
<br /> <span className='bold'>Rating:</span>&nbsp;{rating}/5<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,3 +1,4 @@
.layout { .layout {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -1,31 +1,17 @@
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({ export default function BookReviewItem ({ href, title, author, rating, image, tags }) {
href, const imageUrl = image ? `${process.env.NEXT_PUBLIC_CONTENT_API_BASE_URL}/assets/${image}` : undefined
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} className={style.thumb} style={{
style={{ backgroundImage: `url(${imageUrl ?? '/img/book-placeholder.jpg'})`
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>
@ -33,25 +19,19 @@ export default function BookReviewItem({
<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'
height="15" ><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' />
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,6 +15,7 @@
/* font-weight: bold; */ /* font-weight: bold; */
} }
.thumb { .thumb {
width: auto; width: auto;
height: 290px; height: 290px;

View file

@ -1,34 +1,31 @@
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 <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' />
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,44 +1,37 @@
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 <a href='mailto:me@aaronjy.me'>Send me an email</a>
href="https://git.aaronjy.me/aaron/aaronjy-me" </span>{', '}
target="_blank" <span>
rel="nofollow noopener noreferrer" <a href='https://git.aaronjy.me/aaron/aaronjy-me' target='_blank' rel='nofollow noopener noreferrer'>View site src</a>
>
View site src
</a>
</span> </span>
</div> </div>
<div> <div>
<small> <small>2025 Aaron Yarborough, <span title='major.minior.patch.content'>v{pck.version}</span></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,5 +1,11 @@
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 <div className={style.grid}>{children}</div>; return (
<div
className={style.grid}
>
{children}
</div>
)
} }

View file

@ -1,26 +1,21 @@
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="/writing">Writing</Link> <Link href='/tags'>Tags</Link>{', '}
{", "} <Link href='/cv'>CV</Link>{', '}
<Link href="/tags">Tags</Link> <Link href='/library'>Library</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,36 +1,26 @@
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> <li><a href='#experience'>Professional experience</a></li>
<a href="#experience">Professional experience</a> <li><a href='#competencies'>Competencies</a></li>
</li> <li><a href='#competencies'>Certifications</a></li>
<li> <li><a href='#languages'>Languages</a></li>
<a href="#competencies">Competencies</a> <li><a href='#education'>Education</a></li>
</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}>
@ -42,35 +32,30 @@ function Resume({
> >
{markdownToHtml(exp.description)} {markdownToHtml(exp.description)}
</WorkExperience> </WorkExperience>
{!!exp.skills?.length && ( {!!exp.skills?.length && <details>
<details> <summary>Competencies</summary>
<summary>Competencies</summary> <>{exp.skills.sort().join(', ')}</>
<>{exp.skills.sort().join(", ")}</> </details>}
</details>
)}
</div> </div>
))} ))}
</div> </div>
<div className="sidebar"> <div className='sidebar'>
<h2 id="competencies">Competencies</h2> <h2 id='competencies'>Competencies</h2>
<ul> <ul>
{competencies {competencies?.sort((a, b) => a.name > b.name).map((c, i) => (
?.sort((a, b) => a.name > b.name) <li key={i}>{c.name}</li>
.map((c, i) => ( ))}
<li key={i}>{c.name}</li>
))}
</ul> </ul>
<h2 id="certifications">Certifications</h2> <h2 id='certifications'>Certifications</h2>
<ul> <ul>
{certifications {certifications?.sort((a, b) => a.name > b.name).map((c, i) => (
?.sort((a, b) => a.name > b.name) <li key={i}>{c.name}</li>
.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}>
@ -79,18 +64,19 @@ 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}
@ -102,9 +88,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,17 +1,19 @@
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 (
<div> <table>
{entries <tbody>
.map((e) => ( {entries.map((e) => (
<p key={e.slug}> <tr key={e.slug}>
<Link href={`${urlPrefix}${e.slug}`}>{e.title}</Link>{" "} <td>{!!e.datePublished && <span>{formatDate(e.datePublished)}</span>}</td>
{!!e.datePublished && <i>{`on ${formatDate(e.datePublished)}`}</i>} <td>
</p> <Link href={`${urlPrefix}${e.slug}`}>{e.title}</Link>
)) </td>
.slice(0, max > 0 ? max : entries.length)} </tr>
</div> )).slice(0, max > 0 ? max : entries.length)}
); </tbody>
</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,44 +1,37 @@
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(true)
useEffect( useEffect(function () {
function () { switch (type) {
if (!urlPrefix || max <= 0) return; case 'posts':
(async function () {
const res = await fetchPosts([])
const json = await res.json()
setItems(json.data.sort((a, b) => a.datePublished < b.datePublished) ?? [])
setLoading(false)
})()
break
switch (type) { default:
case "posts": throw `Could not render StaticContentList: content type ${type} not supported.`
(async function () { }
setLoading(true); })
const res = await fetchPosts([]);
const json = await res.json();
setItems(
json.data.sort((a, b) => a.datePublished < b.datePublished) ??
[],
);
setLoading(false);
})();
break;
default:
throw `Could not render StaticContentList: content type ${type} not supported.`;
}
},
[type, urlPrefix, max],
);
return ( 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,84 @@
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 {
FailedFetchBasicPageError, serialize
FailedFetchBasicPagesError, } from 'next-mdx-remote-client/serialize'
} from "@/errors"; import { NextSeo } from 'next-seo'
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 getStaticPaths () {
const { path } = params; // const res = await fetchBasicPages(['path'])
// if (!res.ok) {
// throw new FailedFetchBasicPagesError(await res.text())
// }
// const pages = (await res.json()).data
// const paths = {
// paths: pages.map(page => ({
// params: {
// path: page.path?.split('/') // about/page -> [about, page]
// .filter(p => !!p) ?? [] // deal with paths starting with '/'
// }
// })),
// fallback: true // false or "blocking"
// }
// return paths
// }
export async function getServerSideProps ({ params }) {
const { path } = 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} title={title} description={undefined} openGraph={
description={undefined} {
openGraph={{ title,
title, description: undefined
description: undefined, }
}} }
/> />
<div> <div>
<MDXClient {...mdxSource} components={mdxComponents} /> <MDXClient {...mdxSource} components={mdxComponents} />
</div> </div>
</DefaultLayout> </DefaultLayout>
); )
} }

View file

@ -1,14 +1,12 @@
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 <DefaultSeo defaultTitle='Aaron Yarborough' titleTemplate='%s | Aaron Yarborough' />
defaultTitle="Aaron Yarborough" <Component
titleTemplate="%s | Aaron Yarborough" {...pageProps} />
/>
<Component {...pageProps} />
</> </>
); )
} }

View file

@ -1,20 +1,16 @@
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 <script defer data-domain='aaronjy.me' src='https://analytics.aaronjy.me/js/script.js' />
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,25 +29,26 @@ 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} title={Title} openGraph={
openGraph={{ {
title: Title, title: Title
}} }
}
/> />
<h1>{Title}</h1> <h1>{Title}</h1>
<section> <section>
@ -60,5 +61,5 @@ export default function ResumePage({
/> />
</section> </section>
</DefaultLayout> </DefaultLayout>
); )
} }

View file

@ -1,11 +1,8 @@
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 { import { FailedFetchBookReviewError, FailedFetchBookReviewsError } from '@/errors'
FailedFetchBookReviewError,
FailedFetchBookReviewsError,
} from "@/errors";
// export async function getStaticPaths () { // export async function getStaticPaths () {
// const res = await fetchBookReviews(['slug'], { // const res = await fetchBookReviews(['slug'], {
@ -29,45 +26,45 @@ import {
// } // }
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,39 +1,40 @@
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.sort( const reviews = (await res.json()).data
(a, b) => new Date(b.readDate).getTime() - new Date(a.readDate).getTime(), .sort((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>
@ -41,14 +42,10 @@ export default function Library({ reviews }) {
<section> <section>
<Grid columns={5}> <Grid columns={5}>
{reviews.map((review, _) => ( {reviews.map((review, _) => (
<BookReviewItem <BookReviewItem key={review.title} {...review} href={`/library/${review.slug}`} />
key={review.title}
{...review}
href={`/library/${review.slug}`}
/>
))} ))}
</Grid> </Grid>
</section> </section>
</DefaultLayout> </DefaultLayout>
); )
} }

View file

@ -1,72 +1,69 @@
import { FailedFetchPostsError } from "@/errors"; import { FailedFetchPostsError } from '@/errors'
import DefaultLayout from "@/layouts/DefaultLayout/DefaultLayout"; import DefaultLayout from '@/layouts/DefaultLayout/DefaultLayout'
import { import { fetchItems, fetchPosts, getTagsFromPosts } from '@/services/content-service'
fetchItems, import { NextSeo } from 'next-seo'
fetchPosts, import Link from 'next/link'
getTagsFromPosts, import React from 'react'
} 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) {Object.keys(tags).sort().map(tag => {
.sort() const tagPosts = posts
.map((tag) => { .filter(p => p.tags.includes(tag))
const tagPosts = posts .sort((a, b) => b.title > -a.title)
.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,42 +1,63 @@
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 () {
// const res = await fetchPosts(['slug'], {
// status: 'published'
// })
// if (!res.ok) {
// throw new FailedFetchPostsError(await res.text())
// }
// const posts = (await res.json()).data
// return {
// paths: posts.map(post => ({
// params: {
// slug: post.slug
// }
// })),
// fallback: true // false or "blocking"
// }
// }
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,49 +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,72 +1,71 @@
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) => export const fetchBookReviews = async (...args) => fetchItems('book_review', ...args)
fetchItems("book_review", ...args); export const fetchBasicPages = async (...args) => fetchItems('basic_pages', ...args)
export const fetchBasicPages = async (...args) => export const fetchCV = async (...args) => fetchItems('cv', ...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("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) { export function getTagsFromPosts (posts) {
const allTags = {}; const allTags = {}
for (const post of posts) { for (const post of posts) {
if (!post.tags) { if (!post.tags) { continue }
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))