From 526b13316d99fb253a657512ab9cc4b8639f176d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 23:57:32 +0000 Subject: [PATCH 1/6] Replace ESLint with Biome for linting - Remove eslint and eslint-config-next devDependencies - Add @biomejs/biome ^1.9.4 as devDependency - Create biome.json with recommended rules, a11y, and formatter config - Update lint script from `eslint .` to `biome check .` - Remove .eslintrc.json Closes #267 Co-authored-by: David Johnston --- .eslintrc.json | 3 --- biome.json | 34 ++++++++++++++++++++++++++++++++++ package.json | 7 +++---- 3 files changed, 37 insertions(+), 7 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 biome.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357a..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..7f7bb653 --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "recommended": true + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "es5" + } + }, + "files": { + "ignore": [ + "node_modules", + ".next", + "src/generated", + "public" + ] + } +} diff --git a/package.json b/package.json index 0b5633fc..e4599b81 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "SHOW_TEST_PAGES=true npm run generate:all && next dev", "start": "netlify serve", "next:start": "next start", - "lint": "eslint .", + "lint": "biome check .", "types": "tsc && tsc -p cypress/tsconfig.json", "test:cypress": "cypress open", "test:cypress:ci": "cypress run --browser chrome", @@ -62,9 +62,8 @@ "@vitejs/plugin-react": "^4.3.1", "babel-plugin-react-compiler": "^19.1.0-rc.3", "cypress": "^14.5.0", - "eslint": "^8", - "eslint-config-next": "14.2.4", - "fs-extra": "^11.2.0", + "@biomejs/biome": "^1.9.4", +"fs-extra": "^11.2.0", "jsdom": "^24.1.0", "sentry-upload-sourcemaps": "^8.13.0", "ts-node": "^10.9.2", From 1d5d47aafe2ac61946e6c960da915281f4e0f1a2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 00:12:53 +0000 Subject: [PATCH 2/6] Fix Biome ignore list to exclude generated folders - Add VCS integration with useIgnoreFile: true to respect .gitignore (covers .cache, .netlify, etc.) - Add next-env.d.ts and tsconfig.tsbuildinfo to explicit ignore list Co-authored-by: David Johnston --- biome.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 7f7bb653..1cab8338 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,10 @@ { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, "organizeImports": { "enabled": true }, @@ -28,7 +33,9 @@ "node_modules", ".next", "src/generated", - "public" + "public", + "next-env.d.ts", + "tsconfig.tsbuildinfo" ] } } From ef50b9f4066a94827b7ef13121e799a599b8e600 Mon Sep 17 00:00:00 2001 From: David Johnston Date: Thu, 21 May 2026 16:59:35 +1000 Subject: [PATCH 3/6] Update gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aa7722e8..7d8d99b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store node_modules /.cache From ca93d1eb147fb8aab07c5937e327cd77bdc8a9bd Mon Sep 17 00:00:00 2001 From: David Johnston Date: Thu, 21 May 2026 17:01:01 +1000 Subject: [PATCH 4/6] Run biome check --fix --- .../scripts/wait-for-netlify-deploy.js | 148 +++++----- .vscode/settings.json | 115 ++++---- buildTimeUtils/generateListOfArticles.ts | 73 +++-- cypress/e2e/blogfeatures.cy.ts | 66 +++-- cypress/e2e/realposttests.cy.ts | 138 +++++---- cypress/e2e/smoketests.cy.ts | 191 ++++++------ cypress/support/commands.ts | 3 +- cypress/support/e2e.ts | 4 +- cypress/tsconfig.json | 17 +- next.config.js | 21 +- package.json | 2 +- src/app/about/page.tsx | 185 ++++++------ src/app/ai-policy/page.tsx | 46 +-- src/app/demos/cache/immutable/route.ts | 19 +- .../demos/cache/requires-validation/route.ts | 30 +- .../demos/responsive-cookies/req1/route.ts | 7 +- src/app/drafts/[slug]/page.tsx | 62 ++-- src/app/drafts/page.tsx | 22 +- src/app/error.tsx | 19 +- src/app/game-of-life/page.tsx | 273 +++++++++--------- src/app/global-error.tsx | 8 +- src/app/layout.tsx | 103 ++++--- src/app/not-found.tsx | 12 +- src/app/open-source/page.tsx | 150 +++++----- src/app/page.tsx | 60 ++-- src/app/posts/[slug]/page.tsx | 52 ++-- src/app/posts/page.tsx | 87 +++--- src/app/sandbox/page.tsx | 19 +- src/app/test/[slug]/page.tsx | 62 ++-- src/app/test/page.tsx | 45 +-- src/components/Aside/Aside.tsx | 21 +- .../BlogPostFrame/BlogPostFrame.tsx | 24 +- .../BlogPostFrame/TextHighlightProvider.tsx | 30 +- .../BlogPostFrame/react-text-highlight.css | 241 +++++++--------- .../CodeExampleLink/CodeExampleLink.tsx | 12 +- .../DatePublished/DatePublished.tsx | 27 +- src/components/DemoFrame/DemoFrame.tsx | 22 +- .../EditWithGithub/EditWithGithub.tsx | 34 +-- .../FrontmatterBox/FrontmatterBox.tsx | 164 ++++++----- src/components/ImagePanel/ImagePanel.tsx | 36 +-- src/components/InfoPanel/InfoPanel.tsx | 32 +- .../ListOfArticles/ListOfArticles.tsx | 92 ++++-- .../ListOfTagsPanel/ListOfTagsPanel.tsx | 67 +++-- src/components/Nav/Nav.tsx | 43 ++- src/components/PostComments/PostComments.tsx | 90 +++--- src/components/RaiseAnIssue/RaiseAnIssue.tsx | 29 +- src/components/SheepImage/SheepImage.tsx | 48 +-- .../TextHighlight/TextHighlight.tsx | 16 +- src/components/common.tsx | 10 +- src/components/error_pages/Page404.tsx | 12 +- src/components/error_pages/Page500.tsx | 12 +- .../components/RequestRequiresValidation.tsx | 66 +++-- .../encapsulate-state/AutocompleteDemo.tsx | 260 ++++++++++++----- .../encapsulate-state/SpecialButtonDemo.tsx | 10 +- .../components/MyForm.tsx | 32 +- .../hooks/useConfirmationModal.tsx | 112 +++---- src/demos/react-renders/ReactRenders.tsx | 104 ++++--- src/demos/react-renders/ReactRenders2.tsx | 155 +++++----- src/demos/react-renders/ReactRenders3.tsx | 73 +++-- src/demos/react-renders/ReactRenders3b.tsx | 246 +++++++++------- src/demos/react-renders/ReactRenders3c.tsx | 79 ++--- src/demos/react-renders/ReactRenders4.tsx | 28 +- src/demos/react-renders/ReactRenders5.tsx | 93 +++--- src/demos/react-renders/common.tsx | 42 +-- src/demos/react-renders/index.ts | 2 +- src/demos/react-renders/style.css | 142 +++++---- .../components/UserProfile1.tsx | 46 +-- .../components/UserProfile2.tsx | 48 +-- .../components/UserProfile3.tsx | 51 ++-- .../components/UserProfileRsc.tsx | 26 +- .../responsive_cookies/hooks/useCookie.ts | 58 ++-- .../hooks/useCookieWithListenerPolyfilled.ts | 69 +++-- src/instrumentation.ts | 10 +- src/mdx-components.tsx | 8 +- src/modules.d.ts | 9 +- src/utils/blogPosts.tsx | 265 ++++++++--------- tsconfig.json | 16 +- utils/extractFrontMatter.bin.ts | 26 +- utils/extractFrontMatter.spec.ts | 182 ++++++++---- utils/extractFrontMatter.ts | 129 +++++---- utils/frontmatterTypings.ts | 45 ++- utils/generateImageBarrelFiles.ts | 49 ++-- utils/generateRss.ts | 8 +- utils/generateSitemap.ts | 6 +- utils/getDomainUrl.ts | 8 +- utils/getFileGitTimestamps.ts | 63 ++-- utils/getRss.ts | 48 ++- utils/getSitemap.ts | 40 ++- utils/getSocialMetas.ts | 59 ++-- utils/transformMdx.bin.ts | 32 +- utils/transformMdx.test.ts | 89 +++--- utils/transformMdx.ts | 6 +- utterances.json | 6 +- vitest.config.js | 10 +- 94 files changed, 3192 insertions(+), 2765 deletions(-) diff --git a/.github/workflows/scripts/wait-for-netlify-deploy.js b/.github/workflows/scripts/wait-for-netlify-deploy.js index 07e53789..89cc5a0b 100644 --- a/.github/workflows/scripts/wait-for-netlify-deploy.js +++ b/.github/workflows/scripts/wait-for-netlify-deploy.js @@ -1,24 +1,22 @@ - - [ - "PR_NUMBER", - "BRANCH_NAME", - "NETLIFY_SITE_ID", - "NETLIFY_TOKEN", - "UPDATED_AT", - "COMMIT_SHA" + "PR_NUMBER", + "BRANCH_NAME", + "NETLIFY_SITE_ID", + "NETLIFY_TOKEN", + "UPDATED_AT", + "COMMIT_SHA", ].forEach((v) => { - if (!(process.env[v])) { - throw new Error(`Env var: '${v}' was not provided`) - } -}) + if (!process.env[v]) { + throw new Error(`Env var: '${v}' was not provided`); + } +}); const { - PR_NUMBER, - BRANCH_NAME, - NETLIFY_SITE_ID, - NETLIFY_TOKEN, - UPDATED_AT, - COMMIT_SHA + PR_NUMBER, + BRANCH_NAME, + NETLIFY_SITE_ID, + NETLIFY_TOKEN, + UPDATED_AT, + COMMIT_SHA, } = process.env; // Six minutes @@ -26,66 +24,78 @@ const MAX_NUM_TRIES = 120; const DELAY_TIME_MS = 5000; async function main() { + let numTries = 0; + while (numTries <= MAX_NUM_TRIES) { + numTries++; + await new Promise((res) => setTimeout(res, DELAY_TIME_MS)); + console.info(`Attempt #${numTries}`); + + const pageNum = 1; + const result = await fetch( + `https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys?branch=${BRANCH_NAME}&page=${pageNum}`, + { + headers: { + Authorization: `Bearer ${NETLIFY_TOKEN}`, + }, + } + ); + + if (!result.ok) { + throw new Error(`Result was not ok: ${result.statusText}`); + } - let numTries = 0; - while (numTries <= MAX_NUM_TRIES) { - numTries++; - await new Promise((res) => setTimeout(res, DELAY_TIME_MS)) - console.info(`Attempt #${numTries}`); - - let pageNum = 1; - const result = await fetch(`https://api.netlify.com/api/v1/sites/${NETLIFY_SITE_ID}/deploys?branch=${BRANCH_NAME}&page=${pageNum}`, { - headers: { - "Authorization": `Bearer ${NETLIFY_TOKEN}` - } - }); - - if (!result.ok) { - throw new Error(`Result was not ok: ${result.statusText}`) - } - - const json = await result.json(); - - if (json.length === 100) { - throw new Error("Result length was 100, you have there is probably another page of results") - } - - const filteredResults = json.filter((v) => v.review_id === parseInt(PR_NUMBER) && v.commit_ref === COMMIT_SHA); - console.info(`Found ${filteredResults.length} results with matching review_id and commit_ref`); - - - const filteredResults2 = filteredResults.filter((v) => { - return new Date(v.created_at) > new Date(UPDATED_AT); - }) - - console.info(`Found ${filteredResults2.length} results with matching created_at greater than UPDATED_AT (${UPDATED_AT})`); + const json = await result.json(); - if (filteredResults2.length > 1) { - throw new Error(`Expect only one deploy to exist, got ${filteredResults2.length}`) - } + if (json.length === 100) { + throw new Error( + "Result length was 100, you have there is probably another page of results" + ); + } + const filteredResults = json.filter( + (v) => + v.review_id === Number.parseInt(PR_NUMBER) && + v.commit_ref === COMMIT_SHA + ); + console.info( + `Found ${filteredResults.length} results with matching review_id and commit_ref` + ); + + const filteredResults2 = filteredResults.filter((v) => { + return new Date(v.created_at) > new Date(UPDATED_AT); + }); + + console.info( + `Found ${filteredResults2.length} results with matching created_at greater than UPDATED_AT (${UPDATED_AT})` + ); + + if (filteredResults2.length > 1) { + throw new Error( + `Expect only one deploy to exist, got ${filteredResults2.length}` + ); + } - if (filteredResults2.length === 0) { - console.info("No matching results, deploy isn't created yet."); - continue; - } + if (filteredResults2.length === 0) { + console.info("No matching results, deploy isn't created yet."); + continue; + } - const singleResult = filteredResults2[0]; - const resultStatus = singleResult.state; + const singleResult = filteredResults2[0]; + const resultStatus = singleResult.state; - console.info(`Result state is '${resultStatus}'`) + console.info(`Result state is '${resultStatus}'`); - if (resultStatus === "ready") { - console.info(`Deploy URL: ${singleResult.deploy_ssl_url}`) - process.exit(0); - } + if (resultStatus === "ready") { + console.info(`Deploy URL: ${singleResult.deploy_ssl_url}`); + process.exit(0); + } - if (result.status === "error") { - throw new Error("Deploy had an error status") - } + if (result.status === "error") { + throw new Error("Deploy had an error status"); } + } - throw new Error("Maximum retries exceeded.") + throw new Error("Maximum retries exceeded."); } -main(); +main(); diff --git a/.vscode/settings.json b/.vscode/settings.json index f5749be1..706e51d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,61 +1,60 @@ { - "cSpell.words": [ - "Barfloo", - "blackbox", - "Bloggs", - "brianlow", - "CDKTF", - "codemirror", - "datetimes", - "Deduped", - "devs", - "divs", - "dockerise", - "dockerised", - "Dockerising", - "easymde", - "Foobar", - "Fooby", - "FOUCs", - "frontmatter", - "harfiles", - "importmap", - "interactable", - "jsonplaceholder", - "logmetadata", - "MDE's", - "MITM", - "Mockbin", - "nextjs", - "Oldweb", - "openapi", - "petstore", - "Rehype", - "relitigate", - "Sendgrid", - "statefulness", - "supervisord", - "tanstack", - "testid", - "testids", - "textareas", - "texteditor", - "thunks", - "timezones", - "Todos", - "toolings", - "tsconfigs", - "webp" - ], + "cSpell.words": [ + "Barfloo", + "blackbox", + "Bloggs", + "brianlow", + "CDKTF", + "codemirror", + "datetimes", + "Deduped", + "devs", + "divs", + "dockerise", + "dockerised", + "Dockerising", + "easymde", + "Foobar", + "Fooby", + "FOUCs", + "frontmatter", + "harfiles", + "importmap", + "interactable", + "jsonplaceholder", + "logmetadata", + "MDE's", + "MITM", + "Mockbin", + "nextjs", + "Oldweb", + "openapi", + "petstore", + "Rehype", + "relitigate", + "Sendgrid", + "statefulness", + "supervisord", + "tanstack", + "testid", + "testids", + "textareas", + "texteditor", + "thunks", + "timezones", + "Todos", + "toolings", + "tsconfigs", + "webp" + ], - "[mdx]": { - "editor.defaultFormatter": "unifiedjs.vscode-mdx", - "editor.inlineSuggest.enabled": false, - "editor.quickSuggestions": { - "other": "off", - "comments": "off", - "strings": "off" - } + "[mdx]": { + "editor.defaultFormatter": "unifiedjs.vscode-mdx", + "editor.inlineSuggest.enabled": false, + "editor.quickSuggestions": { + "other": "off", + "comments": "off", + "strings": "off" } - -} \ No newline at end of file + } +} diff --git a/buildTimeUtils/generateListOfArticles.ts b/buildTimeUtils/generateListOfArticles.ts index 07323f8d..ce30a9e8 100644 --- a/buildTimeUtils/generateListOfArticles.ts +++ b/buildTimeUtils/generateListOfArticles.ts @@ -1,60 +1,59 @@ -import { readdir, writeFile, appendFile} from 'node:fs/promises'; +import { appendFile, readdir, writeFile } from "node:fs/promises"; -const PATH_TO_ARTICLES_COMPONENT = "app/generated/ListOfArticles.tsx"; -const PATH_TO_BLOG_POSTS = "app/routes/posts" +const PATH_TO_ARTICLES_COMPONENT = "app/generated/ListOfArticles.tsx"; +const PATH_TO_BLOG_POSTS = "app/routes/posts"; async function generateListOfArticles() { - try { - const files = await readdir(PATH_TO_BLOG_POSTS); + try { + const files = await readdir(PATH_TO_BLOG_POSTS); - - await writeFile(PATH_TO_ARTICLES_COMPONENT, ` + await writeFile( + PATH_TO_ARTICLES_COMPONENT, + ` export const ListOfArticles = () => { - return
    `); - - for (const file of files) { - - if (!file.endsWith(".mdx")){ - console.warn(`Found a non-mdx file in ${PATH_TO_BLOG_POSTS}: ${file}`) - } - - - // TODO proper handling of illegal characters - else if (file.includes(' ')) { - console.warn(`Found a space in file: ${file}`) - } + return
      ` + ); + for (const file of files) { + if (!file.endsWith(".mdx")) { + console.warn(`Found a non-mdx file in ${PATH_TO_BLOG_POSTS}: ${file}`); + } - else { - const fileName = file.split('.mdx')[0]; + // TODO proper handling of illegal characters + else if (file.includes(" ")) { + console.warn(`Found a space in file: ${file}`); + } else { + const fileName = file.split(".mdx")[0]; - await appendFile(PATH_TO_ARTICLES_COMPONENT, ` + await appendFile( + PATH_TO_ARTICLES_COMPONENT, + `
    • ${fileName}
    • - `) - } - - - } + ` + ); + } + } - appendFile(PATH_TO_ARTICLES_COMPONENT, ` + appendFile( + PATH_TO_ARTICLES_COMPONENT, + `
    } - `) - - - } catch (err) { - throw err; - } + ` + ); + } catch (err) { + throw err; + } } generateListOfArticles().then(() => { - console.info(`Successfully generated ${PATH_TO_ARTICLES_COMPONENT}`) -}) \ No newline at end of file + console.info(`Successfully generated ${PATH_TO_ARTICLES_COMPONENT}`); +}); diff --git a/cypress/e2e/blogfeatures.cy.ts b/cypress/e2e/blogfeatures.cy.ts index 71d54d34..ca1fde97 100644 --- a/cypress/e2e/blogfeatures.cy.ts +++ b/cypress/e2e/blogfeatures.cy.ts @@ -1,39 +1,47 @@ - - -// Ignore the hydration error - which appears to be product of Cypress +// Ignore the hydration error - which appears to be product of Cypress // https://github.com/cypress-io/cypress/issues/27204 -// I'm not too happy with this solution though -Cypress.on('uncaught:exception', (err) => { - if ( - err.message.includes('Hydration failed because the initial UI does not match what was rendered on the server.') || - err.message.includes("There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.") - || err.message.includes("Minified React error") - ) { - return false; - } - // Enable uncaught exception failures for other errors - }); - +// I'm not too happy with this solution though +Cypress.on("uncaught:exception", (err) => { + if ( + err.message.includes( + "Hydration failed because the initial UI does not match what was rendered on the server." + ) || + err.message.includes( + "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering." + ) || + err.message.includes("Minified React error") + ) { + return false; + } + // Enable uncaught exception failures for other errors +}); it("400s OK", () => { - cy.request({ url: '/blah', failOnStatusCode: false }).its("status").should("eq", 404); - cy.visit("blah", { failOnStatusCode: false }); - - cy.findByText("404 - Page Not Found").should("exist"); + cy.request({ url: "/blah", failOnStatusCode: false }) + .its("status") + .should("eq", 404); + cy.visit("blah", { failOnStatusCode: false }); -}) + cy.findByText("404 - Page Not Found").should("exist"); +}); it("SiteMap exists", () => { - cy.request({ url: '/sitemap.xml', failOnStatusCode: false }).its("body").should("include", ''); -}) + cy.request({ url: "/sitemap.xml", failOnStatusCode: false }) + .its("body") + .should( + "include", + '' + ); +}); it("robots.txt exists", () => { - cy.request({ url: '/robots.txt', failOnStatusCode: false }).its("body").should("include", 'Sitemap: https://blacksheepcode.com/sitemap.xml'); -}) + cy.request({ url: "/robots.txt", failOnStatusCode: false }) + .its("body") + .should("include", "Sitemap: https://blacksheepcode.com/sitemap.xml"); +}); it("rss.xml exists", () => { - cy.request({ url: '/rss.xml', failOnStatusCode: false }).its("body").should("include", ''); - -}) - - + cy.request({ url: "/rss.xml", failOnStatusCode: false }) + .its("body") + .should("include", ''); +}); diff --git a/cypress/e2e/realposttests.cy.ts b/cypress/e2e/realposttests.cy.ts index 761ea371..d95d737b 100644 --- a/cypress/e2e/realposttests.cy.ts +++ b/cypress/e2e/realposttests.cy.ts @@ -1,65 +1,83 @@ +// Ignore the hydration error - which appears to be product of Cypress +// https://github.com/cypress-io/cypress/issues/27204 +// I'm not too happy with this solution though +Cypress.on("uncaught:exception", (err) => { + if ( + err.message.includes( + "Hydration failed because the initial UI does not match what was rendered on the server." + ) || + err.message.includes( + "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering." + ) || + err.message.includes("Minified React error") || + err.message.includes("Unknown root exit status.") + ) { + return false; + } + // Enable uncaught exception failures for other errors +}); +describe("real pages", () => { + it("code blocks exist", () => { + cy.visit("posts/adding_msw_bundler_to_remix_app_2"); + cy.get(".react-github-permalink").its("length").should("eq", 10); + }); -// Ignore the hydration error - which appears to be product of Cypress -// https://github.com/cypress-io/cypress/issues/27204 -// I'm not too happy with this solution though -Cypress.on('uncaught:exception', (err) => { - if ( - err.message.includes('Hydration failed because the initial UI does not match what was rendered on the server.') || - err.message.includes("There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.") - || err.message.includes("Minified React error") - ||err.message.includes("Unknown root exit status.") - ) { - return false; - } - // Enable uncaught exception failures for other errors + it.skip("comment blocks", () => { + cy.visit("posts/adding_msw_bundler_to_remix_app_2"); + + cy.get("iframe.utterances-frame").should("exist"); }); + it("homepage table of contents links to blog posts correctly", () => { + cy.visit("/"); -describe("real pages", () => { - it("code blocks exist", () => { - cy.visit('posts/adding_msw_bundler_to_remix_app_2'); - cy.get(".react-github-permalink").its("length").should('eq', 10); - }) - - it.skip("comment blocks", () => { - cy.visit('posts/adding_msw_bundler_to_remix_app_2'); - - cy.get("iframe.utterances-frame").should("exist"); - }) - - it("homepage table of contents links to blog posts correctly", () => { - cy.visit('/'); - - cy.findByRole("link", {name: "How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1"}).should("exist").click({force:true}); - - cy.findByText("In this post, we'll we'll talk about how to achieve the same effect for use in a serverless platform, such a Netlify or AWS Lambda.").should('exist'); - - - cy.get('title:contains("How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1")').should("exist"); - cy.get('meta[name="twitter:title"][content="How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1"]').should("exist"); - - cy.get('meta[name="description"][content="If using Remix on a serverless platform such as Netlify we can use a build time compilation and barrel files to access frontmatter metadata."]').should("exist"); - cy.get('meta[name="twitter:description"][content="If using Remix on a serverless platform such as Netlify we can use a build time compilation and barrel files to access frontmatter metadata."]').should("exist"); - - cy.get('meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/blacksheep_100x100.f7b856af.webp"]').should("exist"); - cy.get('meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/blacksheep_100x100.f7b856af.webp"]').should("exist"); - }) - - - it("rss exists", () => { - cy.visit('/'); - - cy.findByRole("link", {name: "rss feed"}).should("exist"); - }) - - it ('custom social image works', () => { - cy.visit('posts/adding_dark_mode_to_the_blog'); - - - - cy.get('meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]').should("exist"); - cy.get('meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]').should("exist"); - - }); -}); \ No newline at end of file + cy.findByRole("link", { + name: "How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1", + }) + .should("exist") + .click({ force: true }); + + cy.findByText( + "In this post, we'll we'll talk about how to achieve the same effect for use in a serverless platform, such a Netlify or AWS Lambda." + ).should("exist"); + + cy.get( + 'title:contains("How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1")' + ).should("exist"); + cy.get( + 'meta[name="twitter:title"][content="How to configure Remix and mdx-bundler for a serverless platform - barrel files approach, Remix v1"]' + ).should("exist"); + + cy.get( + 'meta[name="description"][content="If using Remix on a serverless platform such as Netlify we can use a build time compilation and barrel files to access frontmatter metadata."]' + ).should("exist"); + cy.get( + 'meta[name="twitter:description"][content="If using Remix on a serverless platform such as Netlify we can use a build time compilation and barrel files to access frontmatter metadata."]' + ).should("exist"); + + cy.get( + 'meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/blacksheep_100x100.f7b856af.webp"]' + ).should("exist"); + cy.get( + 'meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/blacksheep_100x100.f7b856af.webp"]' + ).should("exist"); + }); + + it("rss exists", () => { + cy.visit("/"); + + cy.findByRole("link", { name: "rss feed" }).should("exist"); + }); + + it("custom social image works", () => { + cy.visit("posts/adding_dark_mode_to_the_blog"); + + cy.get( + 'meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]' + ).should("exist"); + cy.get( + 'meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]' + ).should("exist"); + }); +}); diff --git a/cypress/e2e/smoketests.cy.ts b/cypress/e2e/smoketests.cy.ts index 36c33f2f..eb33ae38 100644 --- a/cypress/e2e/smoketests.cy.ts +++ b/cypress/e2e/smoketests.cy.ts @@ -1,126 +1,143 @@ - - -// Ignore the hydration error - which appears to be product of Cypress +// Ignore the hydration error - which appears to be product of Cypress // https://github.com/cypress-io/cypress/issues/27204 -// I'm not too happy with this solution though -Cypress.on('uncaught:exception', (err) => { +// I'm not too happy with this solution though +Cypress.on("uncaught:exception", (err) => { if ( - err.message.includes('Hydration failed because the initial UI does not match what was rendered on the server.') || - err.message.includes("There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.") - || err.message.includes("Minified React error") + err.message.includes( + "Hydration failed because the initial UI does not match what was rendered on the server." + ) || + err.message.includes( + "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering." + ) || + err.message.includes("Minified React error") ) { return false; } // Enable uncaught exception failures for other errors }); -describe('Test pages', () => { - it('Basic MDX Rendering', () => { - cy.visit('test/basic_mdx'); +describe("Test pages", () => { + it("Basic MDX Rendering", () => { + cy.visit("test/basic_mdx"); - cy.findByRole("heading", {name: "This is a basic MDX test."}).should("exist"); + cy.findByRole("heading", { name: "This is a basic MDX test." }).should( + "exist" + ); cy.findByText("This is some text").should("exist"); - }) - - it('Image rendering', () => { - cy.visit('/test/images'); - - cy.findByRole("heading", {name: "images"}).should("exist"); - cy.findByRole("img", {name: "this is the image alt text"}).should("exist"); - }) + }); - it('Frontmatter ', () => { - cy.visit('/test/frontmatter'); - - cy.findByRole("heading", {name: "frontmatter"}).should("exist"); - - cy.get('title:contains("I am the title")').should("exist"); - cy.get('meta[name="twitter:title"][content="I am the title"]').should("exist"); - - cy.get('meta[name="description"][content="I am the description"]').should("exist"); - cy.get('meta[name="twitter:description"][content="I am the description"]').should("exist"); + it("Image rendering", () => { + cy.visit("/test/images"); - }) + cy.findByRole("heading", { name: "images" }).should("exist"); + cy.findByRole("img", { name: "this is the image alt text" }).should( + "exist" + ); + }); + it("Frontmatter ", () => { + cy.visit("/test/frontmatter"); - it("external component", () => { - cy.visit('/test/external_component'); + cy.findByRole("heading", { name: "frontmatter" }).should("exist"); - cy.findByText("I contain a react-github-permalink").should("exist") - - // I'm not asserting on actual content it should encounter - // Because we quickly hit the rate limit - cy.get(".react-github-permalink").should("exist"); - }) + cy.get('title:contains("I am the title")').should("exist"); + cy.get('meta[name="twitter:title"][content="I am the title"]').should( + "exist" + ); - it("external component in series", () => { - cy.visit('/test/external_component_in_series'); + cy.get('meta[name="description"][content="I am the description"]').should( + "exist" + ); + cy.get( + 'meta[name="twitter:description"][content="I am the description"]' + ).should("exist"); + }); + it("external component", () => { + cy.visit("/test/external_component"); - // For some reason the presence of the series causes issues with the external components + cy.findByText("I contain a react-github-permalink").should("exist"); - cy.findByRole("link", {name: "Series - Post 1"}).should("exist"); - cy.findByRole("link", {name: "Series - Post 2"}).should("exist"); + // I'm not asserting on actual content it should encounter + // Because we quickly hit the rate limit + cy.get(".react-github-permalink").should("exist"); + }); - cy.findByText("I contain a react-github-permalink").should("exist") + it("external component in series", () => { + cy.visit("/test/external_component_in_series"); - // I'm not asserting on actual content it should encounter - // Because we quickly hit the rate limit - cy.get(".react-github-permalink").should("exist"); - }) + // For some reason the presence of the series causes issues with the external components - it("series - has the right content", () => { - cy.visit('/test/series1'); + cy.findByRole("link", { name: "Series - Post 1" }).should("exist"); + cy.findByRole("link", { name: "Series - Post 2" }).should("exist"); - cy.findByText("I am series - post 1 content").should("exist"); - cy.findByText('I am the series description').should("exist") + cy.findByText("I contain a react-github-permalink").should("exist"); + // I'm not asserting on actual content it should encounter + // Because we quickly hit the rate limit + cy.get(".react-github-permalink").should("exist"); + }); - cy.findByRole("link", {name: "Series - Post 1"}).should("exist"); - cy.findByRole("link", {name: "Series - Post 2"}).should("exist"); + it("series - has the right content", () => { + cy.visit("/test/series1"); - cy.findByRole("link", {name: "Next: Series - Post 2"}).click({force:true}); - cy.findByText("I am series - post 2 content").should("exist"); - cy.findByRole("link", {name: "Next: Series - Post 2"}).should("not.exist"); + cy.findByText("I am series - post 1 content").should("exist"); + cy.findByText("I am the series description").should("exist"); - } - ) + cy.findByRole("link", { name: "Series - Post 1" }).should("exist"); + cy.findByRole("link", { name: "Series - Post 2" }).should("exist"); - it("trailing slash doesn't matter", () => { - cy.visit('test/basic_mdx/'); + cy.findByRole("link", { name: "Next: Series - Post 2" }).click({ + force: true, + }); + cy.findByText("I am series - post 2 content").should("exist"); + cy.findByRole("link", { name: "Next: Series - Post 2" }).should( + "not.exist" + ); + }); - cy.findByRole("heading", {name: "This is a basic MDX test."}).should("exist"); - cy.findByText("This is some text").should("exist"); - }) + it("trailing slash doesn't matter", () => { + cy.visit("test/basic_mdx/"); - it("query params - won't break things", () => { - cy.visit('test/basic_mdx?q=foo'); + cy.findByRole("heading", { name: "This is a basic MDX test." }).should( + "exist" + ); + cy.findByText("This is some text").should("exist"); + }); - cy.findByRole("heading", {name: "This is a basic MDX test."}).should("exist"); - cy.findByText("This is some text").should("exist"); - }) + it("query params - won't break things", () => { + cy.visit("test/basic_mdx?q=foo"); - it("/test will show a test index page", () => { - cy.visit('test').its("status"); + cy.findByRole("heading", { name: "This is a basic MDX test." }).should( + "exist" + ); + cy.findByText("This is some text").should("exist"); + }); - cy.findByText("Test Posts").should("exist") + it("/test will show a test index page", () => { + cy.visit("test").its("status"); - cy.request({url: '/test', failOnStatusCode: false}).its('status').should('be.ok') + cy.findByText("Test Posts").should("exist"); - }) + cy.request({ url: "/test", failOnStatusCode: false }) + .its("status") + .should("be.ok"); + }); - it("comment blocks", () => { - cy.visit('test/basic_mdx?q=foo'); + it("comment blocks", () => { + cy.visit("test/basic_mdx?q=foo"); - cy.get("iframe.utterances-frame").should("exist"); - }) + cy.get("iframe.utterances-frame").should("exist"); + }); - it ('custom social image works', () => { - cy.visit('/test/images'); - - cy.get('meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]').should("exist"); - cy.get('meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]').should("exist"); - - }); + it("custom social image works", () => { + cy.visit("/test/images"); -}) \ No newline at end of file + cy.get( + 'meta[name="twitter:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]' + ).should("exist"); + cy.get( + 'meta[property="og:image"][content="https://blacksheepcode.com/_next/static/media/bsc_dark.3b1c38e3.webp"]' + ).should("exist"); + }); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index f28cc70a..f40a718a 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -36,5 +36,4 @@ // } // } -import '@testing-library/cypress/add-commands' - +import "@testing-library/cypress/add-commands"; diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f80f74f8..6a173d6f 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import "./commands"; // Alternatively you can use CommonJS syntax: -// require('./commands') \ No newline at end of file +// require('./commands') diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 8197c87b..6799fda1 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,10 +1,9 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress", "node","@testing-library/cypress"], - "noEmit":true - }, - "include": ["**/*.ts"], - - } \ No newline at end of file + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "node", "@testing-library/cypress"], + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/next.config.js b/next.config.js index 6e5b1252..614e8b97 100644 --- a/next.config.js +++ b/next.config.js @@ -4,13 +4,14 @@ const { withSentryConfig } = require("@sentry/nextjs"); const nextConfig = { experimental: { reactCompiler: { - compilationMode: 'annotation', + compilationMode: "annotation", }, }, - serverExternalPackages: ['require-in-the-middle', 'import-in-the-middle', '@opentelemetry/instrumentation'], - - - + serverExternalPackages: [ + "require-in-the-middle", + "import-in-the-middle", + "@opentelemetry/instrumentation", + ], // Ignores the opentelemetry warning // see: https://github.com/open-telemetry/opentelemetry-js/issues/4173 @@ -19,20 +20,14 @@ const nextConfig = { { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } ) => { if (isServer) { - config.ignoreWarnings = [ - { module: /opentelemetry/, }, - ] + config.ignoreWarnings = [{ module: /opentelemetry/ }]; } - return config + return config; }, }; - - - // Injected content via Sentry wizard below - module.exports = withSentryConfig(module.exports, { // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options diff --git a/package.json b/package.json index e4599b81..d318d41e 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "babel-plugin-react-compiler": "^19.1.0-rc.3", "cypress": "^14.5.0", "@biomejs/biome": "^1.9.4", -"fs-extra": "^11.2.0", + "fs-extra": "^11.2.0", "jsdom": "^24.1.0", "sentry-upload-sourcemaps": "^8.13.0", "ts-node": "^10.9.2", diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index 8c0f29b7..59d19997 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,100 +1,89 @@ export default function Page() { - return ( -
    -

    Who am I?

    - -

    I am David Johnston, a web developer based in Melbourne, Australia.

    - -

    - The core of my career as been working with JavaScript/TypeScript, Node - and React, and associated tooling (bundlers, compilers, build pipelines - etc). -

    - -

    - I spend a lot of of time thinking how to put the patterns in place such - that developers can be effective, maintain momentum and enjoy their - jobs. -

    - -

    - I am regular presenter at MelbJS. -

    - -

    Outside of code

    - -

    - My interests outside of computers include typewriters, board games, and - pinball. -

    - - Favourite board games -
      -
    • Terra Mystica
    • -
    • War of the Ring
    • -
    • Twilight Struggle
    • -
    • Patchwork
    • -
    - - Favourite Typewriter -
      -
    • IBM Selectric
    • -
    - - Favourite Pinball - -

    - Real talk, if I'm playing on site, I'm probably going to have a lot more fun playing 90s era machines. They tend to be easier and more apparent what you're meant to be doing. -

    - Older machines: -
      -
    • Black Knight 2000
    • -
    • White Water
    • -
    • Getaway 2
    • -
    • Judge Dredd
    • -
    • Twilight Zone
    • -
    - Modern machines: -
      -
    • The Uncanny X-Men
    • -
    • Game of Thrones
    • -
    - - I own a Data East Jurassic Park. -

    Contact

    - - david@blacksheepcode.com - -

    Elsewhere

    - -
    - ); + return ( +
    +

    Who am I?

    +

    I am David Johnston, a web developer based in Melbourne, Australia.

    +

    + The core of my career as been working with JavaScript/TypeScript, Node + and React, and associated tooling (bundlers, compilers, build pipelines + etc). +

    +

    + I spend a lot of of time thinking how to put the patterns in place such + that developers can be effective, maintain momentum and enjoy their + jobs. +

    +

    + I am regular presenter at MelbJS. +

    +

    Outside of code

    +

    + My interests outside of computers include typewriters, board games, and + pinball. +

    + Favourite board games +
      +
    • Terra Mystica
    • +
    • War of the Ring
    • +
    • Twilight Struggle
    • +
    • Patchwork
    • +
    + Favourite Typewriter +
      +
    • IBM Selectric
    • +
    + Favourite Pinball +

    + Real talk, if I'm playing on site, I'm probably going to have + a lot more fun playing 90s era machines. They tend to be easier and more + apparent what you're meant to be doing. +

    + Older machines: +
      +
    • Black Knight 2000
    • +
    • White Water
    • +
    • Getaway 2
    • +
    • Judge Dredd
    • +
    • Twilight Zone
    • +
    + Modern machines: +
      +
    • The Uncanny X-Men
    • +
    • Game of Thrones
    • +
    + I own a Data East Jurassic Park. +

    Contact

    + david@blacksheepcode.com +

    Elsewhere

    + +
    + ); } diff --git a/src/app/ai-policy/page.tsx b/src/app/ai-policy/page.tsx index a7fef84d..6513866d 100644 --- a/src/app/ai-policy/page.tsx +++ b/src/app/ai-policy/page.tsx @@ -1,28 +1,34 @@ export default function AiPolicyPage() { - return
    -

    AI Policy

    + return ( +
    +

    AI Policy

    -

    Last updated: 7th July 2025

    +

    Last updated: 7th July 2025

    -

    - The text content of the blog posts is entirely written by myself. -

    +

    The text content of the blog posts is entirely written by myself.

    -

    - I use AI (Github Copilot code review) to detect and correct typos and spelling errors. -

    +

    + I use AI (Github Copilot code review) to detect and correct typos and + spelling errors. +

    -

    - I do sometimes use AI as a research tool (e.g. 'tell me what technologies solve X problem') and suggestions from AI may make it into a blog post. However the content and subject of blog posts always respresents my genuine thoughts, opinions or experiences on a topic; I do not use AI to suggest topic ideas. -

    +

    + I do sometimes use AI as a research tool (e.g. 'tell me what + technologies solve X problem') and suggestions from AI may make it + into a blog post. However the content and subject of blog posts always + respresents my genuine thoughts, opinions or experiences on a topic; I + do not use AI to suggest topic ideas. +

    -

    - Images may be AI generated - when they are, the alt text reflects it. -

    - -

    - Code snippets may or may not be generated with AI assistance; code snippets are a genuine representation the code I write in both professional and personal contexts. -

    +

    + Images may be AI generated - when they are, the alt text reflects it. +

    +

    + Code snippets may or may not be generated with AI assistance; code + snippets are a genuine representation the code I write in both + professional and personal contexts. +

    -} \ No newline at end of file + ); +} diff --git a/src/app/demos/cache/immutable/route.ts b/src/app/demos/cache/immutable/route.ts index 1904fe8c..145fa43e 100644 --- a/src/app/demos/cache/immutable/route.ts +++ b/src/app/demos/cache/immutable/route.ts @@ -1,10 +1,13 @@ -import { NextResponse, NextRequest } from "next/server"; -import {hri} from "human-readable-ids" +import { hri } from "human-readable-ids"; +import { type NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { - - return NextResponse.json({ data: hri.random() }, { status: 200, - headers: { - "Cache-Control": "public, max-age=604800, immutable" - } }) - + return NextResponse.json( + { data: hri.random() }, + { + status: 200, + headers: { + "Cache-Control": "public, max-age=604800, immutable", + }, + } + ); } diff --git a/src/app/demos/cache/requires-validation/route.ts b/src/app/demos/cache/requires-validation/route.ts index 868694f2..3b3e6a49 100644 --- a/src/app/demos/cache/requires-validation/route.ts +++ b/src/app/demos/cache/requires-validation/route.ts @@ -1,18 +1,20 @@ -import { NextResponse, NextRequest } from "next/server"; -import {hri} from "human-readable-ids" +import { hri } from "human-readable-ids"; +import { type NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest) { - - - if(request.headers.get("if-none-match") ==="i-am-an-etag"){ - return new NextResponse(null, { - status: 304 - }); + if (request.headers.get("if-none-match") === "i-am-an-etag") { + return new NextResponse(null, { + status: 304, + }); } - return NextResponse.json({ data: hri.random() }, { status: 200, - headers: { - "ETag": "i-am-an-etag", - "Cache-Control": "public, max-age=604800, no-cache" - } }) - + return NextResponse.json( + { data: hri.random() }, + { + status: 200, + headers: { + ETag: "i-am-an-etag", + "Cache-Control": "public, max-age=604800, no-cache", + }, + } + ); } diff --git a/src/app/demos/responsive-cookies/req1/route.ts b/src/app/demos/responsive-cookies/req1/route.ts index cb1af9a1..eb48c06c 100644 --- a/src/app/demos/responsive-cookies/req1/route.ts +++ b/src/app/demos/responsive-cookies/req1/route.ts @@ -1,10 +1,9 @@ +import { hri } from "human-readable-ids"; import { NextResponse } from "next/server"; -import {hri} from "human-readable-ids" export async function GET() { - const x = NextResponse.json({ error: 'I am req 1 data' }, { status: 200 }) - + const x = NextResponse.json({ error: "I am req 1 data" }, { status: 200 }); + x.cookies.set("server-cookie", hri.random()); return x; - } diff --git a/src/app/drafts/[slug]/page.tsx b/src/app/drafts/[slug]/page.tsx index f87ebe17..c934b7bf 100644 --- a/src/app/drafts/[slug]/page.tsx +++ b/src/app/drafts/[slug]/page.tsx @@ -1,38 +1,42 @@ import { BlogPostFrame } from "@/components/BlogPostFrame/BlogPostFrame"; -import { getAllPostFrontmatter, getBlogContent, getMetadata } from "@/utils/blogPosts"; +import { + getAllPostFrontmatter, + getBlogContent, + getMetadata, +} from "@/utils/blogPosts"; import { notFound } from "next/navigation"; -import { PropsWithChildren } from "react"; +import type { PropsWithChildren } from "react"; export async function generateStaticParams() { - - const allFrontMatter = await getAllPostFrontmatter("drafts"); - return allFrontMatter.map((v) => { - - return { slug: v.slug.replace("drafts/", "") } - }) + const allFrontMatter = await getAllPostFrontmatter("drafts"); + return allFrontMatter.map((v) => { + return { slug: v.slug.replace("drafts/", "") }; + }); } -export async function generateMetadata(props: { params: Promise<{ - slug: string -}> }) { - - const params = await props.params; - return getMetadata(`/drafts/${params.slug}`); +export async function generateMetadata(props: { + params: Promise<{ + slug: string; + }>; +}) { + const params = await props.params; + return getMetadata(`/drafts/${params.slug}`); } -export default async function PageLayout(props: PropsWithChildren<{ +export default async function PageLayout( + props: PropsWithChildren<{ params: Promise<{ - slug: string - }> -}>) { - if(process.env.SHOW_DRAFT_PAGES !== "true") { - notFound(); - } - - const params = await props.params; - const content = await getBlogContent( params.slug,"drafts"); - return - {content} - - -} \ No newline at end of file + slug: string; + }>; + }> +) { + if (process.env.SHOW_DRAFT_PAGES !== "true") { + notFound(); + } + + const params = await props.params; + const content = await getBlogContent(params.slug, "drafts"); + return ( + {content} + ); +} diff --git a/src/app/drafts/page.tsx b/src/app/drafts/page.tsx index d2a949ca..1d1f4e51 100644 --- a/src/app/drafts/page.tsx +++ b/src/app/drafts/page.tsx @@ -1,20 +1,20 @@ -import { getAllPostFrontmatter } from "@/utils/blogPosts"; import { ListOfArticles } from "@/components/ListOfArticles/ListOfArticles"; +import { getAllPostFrontmatter } from "@/utils/blogPosts"; import { notFound } from "next/navigation"; async function getAllArticles() { - return getAllPostFrontmatter("drafts") + return getAllPostFrontmatter("drafts"); } export default async function PageLayout() { + if (process.env.SHOW_DRAFT_PAGES !== "true") { + notFound(); + } - if(process.env.SHOW_DRAFT_PAGES !== "true") { - notFound(); - } - - const articles = await getAllArticles(); - return
    - - + const articles = await getAllArticles(); + return ( +
    +
    -} \ No newline at end of file + ); +} diff --git a/src/app/error.tsx b/src/app/error.tsx index cf4ba6b7..d05e9292 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,15 +1,14 @@ -'use client' // Error components must be Client Components +"use client"; // Error components must be Client Components -import { Page500 } from '@/components/error_pages/Page500' -import { useEffect } from 'react' +import { Page500 } from "@/components/error_pages/Page500"; +import { useEffect } from "react"; export default function Error({ - error, - reset, + error, + reset, }: { - error: Error & { digest?: string } - reset: () => void + error: Error & { digest?: string }; + reset: () => void; }) { - - return -} \ No newline at end of file + return ; +} diff --git a/src/app/game-of-life/page.tsx b/src/app/game-of-life/page.tsx index a1ce65f2..e4c0d5f5 100644 --- a/src/app/game-of-life/page.tsx +++ b/src/app/game-of-life/page.tsx @@ -1,171 +1,170 @@ -"use client" -import React, { useState } from 'react'; -import { useInterval } from 'usehooks-ts'; -import { RaiseAnIssue } from '@/components/RaiseAnIssue/RaiseAnIssue'; -export type GameOfLifeProps = { -}; - - +"use client"; +import { RaiseAnIssue } from "@/components/RaiseAnIssue/RaiseAnIssue"; +import React, { useState } from "react"; +import { useInterval } from "usehooks-ts"; +export type GameOfLifeProps = {}; const SIZE = 20; const TICK_RATE = 100; - function generateEmptyMap(height: number, width: number) { - - return new Array(height).fill(false).map((v) => { - return new Array(width).fill(false); - }) + return new Array(height).fill(false).map((v) => { + return new Array(width).fill(false); + }); } -function toggleValueAt(map: Array>, y: number, x: number,) { - - - if (map[y] === undefined) { - throw new Error(`y: '${y} out of range `); - } - - if (map[y][x] === undefined) { - throw new Error(`x: '${x} out of range `); - - } - +function toggleValueAt(map: Array>, y: number, x: number) { + if (map[y] === undefined) { + throw new Error(`y: '${y} out of range `); + } + if (map[y][x] === undefined) { + throw new Error(`x: '${x} out of range `); + } - const map2 = JSON.parse(JSON.stringify(map)); - map2[y][x] = !map2[y][x]; - - return map2; - + const map2 = JSON.parse(JSON.stringify(map)); + map2[y][x] = !map2[y][x]; + return map2; } -function getLivingNeighboursCount(map: Array>, y: number, x: number): number { - - const yMax = map.length - 1; - const xMax = map[0].length - 1; - - - const yIndexsToUse = [y]; - const xIndexesToUse = [x]; - if (y !== 0) { - yIndexsToUse.push(y - 1); - } - if (y !== yMax) { - yIndexsToUse.push(y + 1); +function getLivingNeighboursCount( + map: Array>, + y: number, + x: number +): number { + const yMax = map.length - 1; + const xMax = map[0].length - 1; + + const yIndexsToUse = [y]; + const xIndexesToUse = [x]; + if (y !== 0) { + yIndexsToUse.push(y - 1); + } + if (y !== yMax) { + yIndexsToUse.push(y + 1); + } + if (x !== 0) { + xIndexesToUse.push(x - 1); + } + if (x !== xMax) { + xIndexesToUse.push(x + 1); + } + + let count = 0; + for (const yi of yIndexsToUse) { + for (const xi of xIndexesToUse) { + if (map[yi][xi] && !(yi === y && xi === x)) { + count++; + } } - if (x !== 0) { - xIndexesToUse.push(x - 1); - } - if (x !== xMax) { - xIndexesToUse.push(x + 1); - } - + } - let count = 0; - for (let yi of yIndexsToUse) { - for (let xi of xIndexesToUse) { - if (map[yi][xi] && !(yi === y && xi === x)) { - count++ - } - } - } - - return count; + return count; } function processMap(map: Array>): Array> { - const yMax = map.length; - const xMax = map[0].length; - - const map2 = JSON.parse(JSON.stringify(map)); - - for (let y = 0; y < yMax; y++) { - for (let x = 0; x < xMax; x++) { - - const count = getLivingNeighboursCount(map, y, x); - const selfIsAlive = map[y][x]; - - if (selfIsAlive) { - if (count === 0 || count === 1 || count === 4) { - map2[y][x] = false; - } - } else { - if (count === 3) { - map2[y][x] = true; - } - } + const yMax = map.length; + const xMax = map[0].length; + const map2 = JSON.parse(JSON.stringify(map)); + for (let y = 0; y < yMax; y++) { + for (let x = 0; x < xMax; x++) { + const count = getLivingNeighboursCount(map, y, x); + const selfIsAlive = map[y][x]; + if (selfIsAlive) { + if (count === 0 || count === 1 || count === 4) { + map2[y][x] = false; + } + } else { + if (count === 3) { + map2[y][x] = true; } + } } + } - return map2; + return map2; } export const GameOfLife = (props: GameOfLifeProps) => { + const [map, setMap] = useState(generateEmptyMap(SIZE, SIZE)); - const [map, setMap] = useState(generateEmptyMap(SIZE, SIZE)); - - const [isRunning, setIsRunning] = useState(false); - - - useInterval(() => { - const newMap = processMap(map); - setMap(newMap); - }, isRunning ? TICK_RATE : null); - - function handleCellClick(rowNumber: number, cellNumber: number) { - - if (isRunning) { - setIsRunning(false); - } - setMap(toggleValueAt(map, rowNumber, cellNumber)) - } + const [isRunning, setIsRunning] = useState(false); - function handleStart() { - setIsRunning(!isRunning); - } + useInterval( + () => { + const newMap = processMap(map); + setMap(newMap); + }, + isRunning ? TICK_RATE : null + ); - function handleReset() { - setIsRunning(false); - setMap(generateEmptyMap(SIZE, SIZE)); + function handleCellClick(rowNumber: number, cellNumber: number) { + if (isRunning) { + setIsRunning(false); } - - - - return ( -
    - - - -

    Conway's Game of Life

    -

    Conway's game of life is a classic zero player game that makes for a fun programming exercise.

    -

    See the wikipedia article here.

    -

    Maybe we can play around with alternative rule sets, or investigate performance optimisation.

    -
    - - - -
    -
    - {map.map((row, rowIndex) => { - return
    - {row.map((cell, cellIndex) => { - return
    { - handleCellClick(rowIndex, cellIndex) - }}> - -
    - })} -
    - })} + setMap(toggleValueAt(map, rowNumber, cellNumber)); + } + + function handleStart() { + setIsRunning(!isRunning); + } + + function handleReset() { + setIsRunning(false); + setMap(generateEmptyMap(SIZE, SIZE)); + } + + return ( +
    +

    Conway's Game of Life

    +

    + Conway's game of life is a classic zero player game that makes for + a fun programming exercise. +

    +

    + See the{" "} + + wikipedia article here. + +

    +

    + Maybe we can play around with alternative rule sets, or investigate + performance optimisation. +

    +
    + + +
    +
    + {map.map((row, rowIndex) => { + return ( +
    + {row.map((cell, cellIndex) => { + return ( +
    { + handleCellClick(rowIndex, cellIndex); + }} + >
    + ); + })}
    + ); + })} +
    - -
    - ); + +
    + ); }; -export default GameOfLife; \ No newline at end of file +export default GameOfLife; diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index fc254595..147c2efc 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,7 +1,7 @@ "use client"; import * as Sentry from "@sentry/nextjs"; -import Error from "next/error"; +import type Error from "next/error"; import { useEffect } from "react"; export default function GlobalError({ error }: { error: Error }) { @@ -11,9 +11,7 @@ export default function GlobalError({ error }: { error: Error }) { return ( - - {/* Your Error component here... */} - + {/* Your Error component here... */} ); -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b0ea2ad3..c069bbe2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,31 +5,29 @@ import "react-github-permalink/dist/github-permalink.css"; import { mergeFrontmatterAndDefaultMetadata } from "@/utils/blogPosts"; import * as Sentry from "@sentry/nextjs"; -import { githubPermalinkRscConfig } from "react-github-permalink/dist/rsc"; import { Nav } from "@/components/Nav/Nav"; +import { githubPermalinkRscConfig } from "react-github-permalink/dist/rsc"; const inter = Inter({ subsets: ["latin"] }); -import React from "react"; import { MyTextHighlightProvider } from "@/components/BlogPostFrame/TextHighlightProvider"; +import type React from "react"; githubPermalinkRscConfig.setConfig({ // Can't use the prefix GITHUB in github actions so just have a second token just for github actions githubToken: process.env.GITHUB_TOKEN ?? process.env.PERMALINK_READ_TOKEN, - onError: ((err) => { + onError: (err) => { Sentry.captureException(err); - }) -}) + }, +}); export const metadata: Metadata = mergeFrontmatterAndDefaultMetadata({ title: "Black Sheep Code", - description: "A blog about modern web development" -} -) + description: "A blog about modern web development", +}); export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - return ( @@ -40,11 +38,28 @@ export default function RootLayout({ - - - - - + + + + + + > -
    - - -
    - - {children} - + {children} ); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 1a3126b2..a47979dd 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,8 +1,6 @@ -import { Page404 } from '@/components/error_pages/Page404' -import Link from 'next/link' - +import { Page404 } from "@/components/error_pages/Page404"; +import Link from "next/link"; + export default function NotFound() { - return ( - - ) -} \ No newline at end of file + return ; +} diff --git a/src/app/open-source/page.tsx b/src/app/open-source/page.tsx index 346801c8..5a911d88 100644 --- a/src/app/open-source/page.tsx +++ b/src/app/open-source/page.tsx @@ -1,78 +1,84 @@ import { TextHighlight } from "@blacksheepcode/react-text-highlight"; export default function OpenSource() { - return ( -
    -

    Open Source Projects

    -
      -
    • - - react-github-permalink - -

      Provide a Github permalink and this React component will display the - codeblock. I use this component regularly in my blog. -

      -
    • -
    • - - react-text-highlight - -

      - Highlight some text and show a corresponding comment in the page margin. -

      -
    • -
    • - - use-cookie-state - -

      - A useState like React hook that is responsive to cookie changes that - occur outside of the React context. Includes polyfill for browsers - that do not support the CookieStore API. -

      -
    • + return ( +
      +

      Open Source Projects

      +
        +
      • + + react-github-permalink + +

        + {" "} + Provide a Github permalink and this React component will display the + codeblock. I use this component regularly in my blog. +

        +
      • +
      • + + react-text-highlight + +

        + Highlight some text and show a{" "} + + corresponding comment + {" "} + in the page margin. +

        +
      • +
      • + + use-cookie-state + +

        + A useState like React hook that is responsive to cookie changes that + occur outside of the React context. Includes polyfill for browsers + that do not support the CookieStore API. +

        +
      • -
      • - - TypeScript Tutorial Series - -

        - A TypeScript tutorial series, complete with interactive exercises, - starting from the very basics and going up to generics and mapped - and index types. -

        -
      • +
      • + + TypeScript Tutorial Series + +

        + A TypeScript tutorial series, complete with interactive exercises, + starting from the very basics and going up to generics and mapped + and index types. +

        +
      • -
      • - - Javascript 101 - -

        - A JavaScript tutorial series for people who know nothing about - coding. Complete with interactive exercises. -

        -
      • -
      -
      - ); +
    • + + Javascript 101 + +

      + A JavaScript tutorial series for people who know nothing about + coding. Complete with interactive exercises. +

      +
    • +
    +
    + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3ad0f744..5b76d2bf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,40 +4,40 @@ import { SheepImage } from "@/components/SheepImage/SheepImage"; import { getAllPostFrontmatter } from "@/utils/blogPosts"; async function getAllArticles() { - return getAllPostFrontmatter(); + return getAllPostFrontmatter(); } export default async function Home() { - const articles = await getAllArticles(); + const articles = await getAllArticles(); - return ( - <> -
    -
    - -
    -

    Black Sheep Code

    -

    A blog about modern web development.

    -
    -
    -
    + return ( + <> +
    +
    + +
    +

    Black Sheep Code

    +

    A blog about modern web development.

    +
    +
    +
    -
    - -

    Blog

    - -
    +
    + +

    Blog

    + +
    -

    - I support open source:{" "} - - Open Collective - -

    - - ); +

    + I support open source:{" "} + + Open Collective + +

    + + ); } diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx index 32707ba4..886f898b 100644 --- a/src/app/posts/[slug]/page.tsx +++ b/src/app/posts/[slug]/page.tsx @@ -1,35 +1,37 @@ import { BlogPostFrame } from "@/components/BlogPostFrame/BlogPostFrame"; -import { getMetadata, getAllPostFrontmatter, getBlogContent } from "@/utils/blogPosts"; -import { PropsWithChildren } from "react"; +import { + getAllPostFrontmatter, + getBlogContent, + getMetadata, +} from "@/utils/blogPosts"; +import type { PropsWithChildren } from "react"; export async function generateStaticParams() { - - const allFrontMatter = await getAllPostFrontmatter(); - return allFrontMatter.map((v) => { - - return { slug: v.slug.replace("posts/", "") } - }) + const allFrontMatter = await getAllPostFrontmatter(); + return allFrontMatter.map((v) => { + return { slug: v.slug.replace("posts/", "") }; + }); } export async function generateMetadata(props: { - params: Promise<{ - slug: string - }> + params: Promise<{ + slug: string; + }>; }) { - const params = await props.params; - return getMetadata(`/posts/${params.slug}`); + const params = await props.params; + return getMetadata(`/posts/${params.slug}`); } -export default async function PageLayout(props: PropsWithChildren<{ +export default async function PageLayout( + props: PropsWithChildren<{ params: Promise<{ - slug: string - }> -}>) { - - const params = await props.params; - const content = await getBlogContent( params.slug,"posts"); - return - {content} - - -} \ No newline at end of file + slug: string; + }>; + }> +) { + const params = await props.params; + const content = await getBlogContent(params.slug, "posts"); + return ( + {content} + ); +} diff --git a/src/app/posts/page.tsx b/src/app/posts/page.tsx index d93f4ea7..80653d9d 100644 --- a/src/app/posts/page.tsx +++ b/src/app/posts/page.tsx @@ -1,52 +1,53 @@ -import { getAllPostFrontmatter } from "@/utils/blogPosts"; import { ListOfArticles } from "@/components/ListOfArticles/ListOfArticles"; - +import { getAllPostFrontmatter } from "@/utils/blogPosts"; type Options = { - tagFilter?: string; -} -async function getAllArticles(options?: Options) : Promise<{ - options?: Options, - didFindArticles: boolean; - articles: Array<{slug: string, frontmatter: any}>}> { - - - const result = await getAllPostFrontmatter(); - - if(!options || !options.tagFilter) { - return { - articles: result, - didFindArticles: true, - options: options - }; - } - - const filteredResults = result.filter((v) => { - return v.frontmatter.tags?.includes(options.tagFilter as string); - }); - + tagFilter?: string; +}; +async function getAllArticles(options?: Options): Promise<{ + options?: Options; + didFindArticles: boolean; + articles: Array<{ slug: string; frontmatter: any }>; +}> { + const result = await getAllPostFrontmatter(); + + if (!options || !options.tagFilter) { return { - options: options, - didFindArticles: filteredResults.length > 0, - articles: filteredResults.length > 0 ? filteredResults : result - } + articles: result, + didFindArticles: true, + options: options, + }; + } + + const filteredResults = result.filter((v) => { + return v.frontmatter.tags?.includes(options.tagFilter as string); + }); + + return { + options: options, + didFindArticles: filteredResults.length > 0, + articles: filteredResults.length > 0 ? filteredResults : result, + }; } - export default async function PageLayout({ - searchParams -} : { - searchParams: Promise> + searchParams, +}: { + searchParams: Promise>; }) { - - const params = await searchParams; - const articlesResult = await getAllArticles({ - tagFilter: params?.tag - }); - return
    - + const params = await searchParams; + const articlesResult = await getAllArticles({ + tagFilter: params?.tag, + }); + return ( +
    +
    -} \ No newline at end of file + ); +} diff --git a/src/app/sandbox/page.tsx b/src/app/sandbox/page.tsx index 1770f6c5..670bdb1d 100644 --- a/src/app/sandbox/page.tsx +++ b/src/app/sandbox/page.tsx @@ -1,11 +1,16 @@ -import { ReactRenders1, ReactRenders2, ReactRenders3, ReactRenders5 } from "@/demos/react-renders"; +import { + ReactRenders1, + ReactRenders2, + ReactRenders3, + ReactRenders5, +} from "@/demos/react-renders"; export default function SandBox() { - return
    -

    - This is a sandbox page. -

    + return ( +
    +

    This is a sandbox page.

    - +
    -} \ No newline at end of file + ); +} diff --git a/src/app/test/[slug]/page.tsx b/src/app/test/[slug]/page.tsx index de819708..d19f6609 100644 --- a/src/app/test/[slug]/page.tsx +++ b/src/app/test/[slug]/page.tsx @@ -1,38 +1,42 @@ import { BlogPostFrame } from "@/components/BlogPostFrame/BlogPostFrame"; -import { getAllPostFrontmatter, getBlogContent, getMetadata } from "@/utils/blogPosts"; +import { + getAllPostFrontmatter, + getBlogContent, + getMetadata, +} from "@/utils/blogPosts"; import { notFound } from "next/navigation"; -import { PropsWithChildren } from "react"; +import type { PropsWithChildren } from "react"; export async function generateStaticParams() { - - const allFrontMatter = await getAllPostFrontmatter("test"); - return allFrontMatter.map((v) => { - - return { slug: v.slug.replace("test/", "") } - }) + const allFrontMatter = await getAllPostFrontmatter("test"); + return allFrontMatter.map((v) => { + return { slug: v.slug.replace("test/", "") }; + }); } -export async function generateMetadata(props: { params: Promise<{ - slug: string -}> }) { - const params = await props.params; - return getMetadata(`/test/${params.slug}`); +export async function generateMetadata(props: { + params: Promise<{ + slug: string; + }>; +}) { + const params = await props.params; + return getMetadata(`/test/${params.slug}`); } -export default async function PageLayout(props: PropsWithChildren<{ +export default async function PageLayout( + props: PropsWithChildren<{ params: Promise<{ - slug: string - }> -}>) { - - if(process.env.SHOW_TEST_PAGES !== "true") { - notFound(); - } - - const params = await props.params; - const content = await getBlogContent( params.slug,"test"); - return - {content} - - -} \ No newline at end of file + slug: string; + }>; + }> +) { + if (process.env.SHOW_TEST_PAGES !== "true") { + notFound(); + } + + const params = await props.params; + const content = await getBlogContent(params.slug, "test"); + return ( + {content} + ); +} diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx index 57ce2ecd..4c841991 100644 --- a/src/app/test/page.tsx +++ b/src/app/test/page.tsx @@ -1,32 +1,33 @@ -import { getAllPostFrontmatter } from "@/utils/blogPosts"; -import { ListOfArticles } from "@/components/ListOfArticles/ListOfArticles"; -import { notFound } from "next/navigation"; -import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; import { CodeExampleLink } from "@/components/CodeExampleLink/CodeExampleLink"; +import { ListOfArticles } from "@/components/ListOfArticles/ListOfArticles"; import { MyForm } from "@/demos/imperative_confirmation_modal/components/MyForm"; import { ReactRenders1 } from "@/demos/react-renders/ReactRenders"; import { ReactRenders2 } from "@/demos/react-renders/ReactRenders2"; +import { getAllPostFrontmatter } from "@/utils/blogPosts"; +import { notFound } from "next/navigation"; +import { GithubPermalinkRsc } from "react-github-permalink/dist/rsc"; async function getAllArticles() { - return getAllPostFrontmatter("test") + return getAllPostFrontmatter("test"); } export default async function PageLayout() { - if (process.env.SHOW_TEST_PAGES !== "true") { - notFound(); - } - const articles = await getAllArticles(); - return
    - - Test Posts - - - - - - - + if (process.env.SHOW_TEST_PAGES !== "true") { + notFound(); + } + const articles = await getAllArticles(); + return ( +
    + Test Posts + + + + + +
    -} \ No newline at end of file + ); +} diff --git a/src/components/Aside/Aside.tsx b/src/components/Aside/Aside.tsx index 72672517..bd4d9971 100644 --- a/src/components/Aside/Aside.tsx +++ b/src/components/Aside/Aside.tsx @@ -1,17 +1,14 @@ -import { PropsWithChildren } from "react"; - -type AsideProps = PropsWithChildren<{ - -}> +import type { PropsWithChildren } from "react"; +type AsideProps = PropsWithChildren<{}>; export function Aside(props: AsideProps) { + return ( +
    +
    +
    - - return
    -
    -
    - - {props.children} + {props.children}
    -} \ No newline at end of file + ); +} diff --git a/src/components/BlogPostFrame/BlogPostFrame.tsx b/src/components/BlogPostFrame/BlogPostFrame.tsx index 7553165f..8875d990 100644 --- a/src/components/BlogPostFrame/BlogPostFrame.tsx +++ b/src/components/BlogPostFrame/BlogPostFrame.tsx @@ -1,20 +1,18 @@ -import { Suspense, type PropsWithChildren } from "react"; -import { FrontmatterBox } from "../FrontmatterBox/FrontmatterBox"; +import { type PropsWithChildren, Suspense } from "react"; import { EditWithGithub } from "../EditWithGithub/EditWithGithub"; +import { FrontmatterBox } from "../FrontmatterBox/FrontmatterBox"; import PostComments from "../PostComments/PostComments"; // Removed unused import export function BlogPostFrame(props: PropsWithChildren<{ pathname: string }>) { - return <> - - - {props.children} - - - - - - + return ( + <> + {props.children} + + + + -} \ No newline at end of file + ); +} diff --git a/src/components/BlogPostFrame/TextHighlightProvider.tsx b/src/components/BlogPostFrame/TextHighlightProvider.tsx index 176dcfe3..2f849a14 100644 --- a/src/components/BlogPostFrame/TextHighlightProvider.tsx +++ b/src/components/BlogPostFrame/TextHighlightProvider.tsx @@ -1,25 +1,17 @@ -"use client" +"use client"; import "./react-text-highlight.css"; - -import { useRef } from "react"; import { TextHighlightProvider } from "@blacksheepcode/react-text-highlight"; +import { useRef } from "react"; export function MyTextHighlightProvider(props: React.PropsWithChildren<{}>) { - - - const ref = useRef(null); - return
    - -
    - -
    - -
    - {props.children} -
    -
    -
    - -
    + const ref = useRef(null); + return ( +
    +
    + +
    {props.children}
    +
    +
    + ); } diff --git a/src/components/BlogPostFrame/react-text-highlight.css b/src/components/BlogPostFrame/react-text-highlight.css index d1c7e724..037c53bd 100644 --- a/src/components/BlogPostFrame/react-text-highlight.css +++ b/src/components/BlogPostFrame/react-text-highlight.css @@ -1,177 +1,152 @@ :root { - - - --highlight-bg-color: #fde05f; - --highlight-text-color: #000000; - --highlight-bg-color-hover: #fde05f; - --highlight-border-hover: rgb(186, 151, 105); - --highlight-border-selected: rgb(225, 139, 25); - --highlight-comment-box-shadow: rgba(0, 0, 0, 0.2); - --highlight-comment-bg: #f6e69c; - - - + --highlight-bg-color: #fde05f; + --highlight-text-color: #000000; + --highlight-bg-color-hover: #fde05f; + --highlight-border-hover: rgb(186, 151, 105); + --highlight-border-selected: rgb(225, 139, 25); + --highlight-comment-box-shadow: rgba(0, 0, 0, 0.2); + --highlight-comment-bg: #f6e69c; } - .page-with-margins { - display: flex; - flex-flow: row nowrap; - gap: 6px; - - .main-column { - flex: 0 1 var(--column-width) - } + display: flex; + flex-flow: row nowrap; + gap: 6px; - .left-gutter { - - flex: 1 1 auto; - } + .main-column { + flex: 0 1 var(--column-width); + } + .left-gutter { + flex: 1 1 auto; + } } - .right-gutter { - display: flex; - flex-direction: column; - flex-wrap: nowrap; - flex: 1 1 auto; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + flex: 1 1 auto; - - /** + /** Can't use vars in media queries? wtf. https://stackoverflow.com/questions/40722882/css-native-variables-not-working-in-media-queries */ - @media screen and (max-width: 985px) { - - display: block; - position: fixed; - bottom: 0; - left: 0; - right: 0; - - } + @media screen and (max-width: 985px) { + display: block; + position: fixed; + bottom: 0; + left: 0; + right: 0; + } } @media (prefers-color-scheme: dark) { - :root { - - --highlight-bg-color: #5790d459; - --highlight-text-color: #fff; - --highlight-bg-color-hover: #4a4b80; - --highlight-border-hover: #5b5f99; - --highlight-border-selected: #9178e2; + :root { + --highlight-bg-color: #5790d459; + --highlight-text-color: #fff; + --highlight-bg-color-hover: #4a4b80; + --highlight-border-hover: #5b5f99; + --highlight-border-selected: #9178e2; - --highlight-comment-box-shadow: rgba(0, 0, 0, 0.2); - --highlight-comment-bg: #223253; - } + --highlight-comment-box-shadow: rgba(0, 0, 0, 0.2); + --highlight-comment-bg: #223253; + } } - .text-highlight { - background-color: var(--highlight-bg-color); - color: var(--highlight-text-color); - border: solid 1px transparent; + background-color: var(--highlight-bg-color); + color: var(--highlight-text-color); + border: solid 1px transparent; } .text-highlight-comment { - background-color: var(--highlight-comment-bg); - color: var(--highlight-text-color); - box-sizing: border-box; - - margin: 2px; + background-color: var(--highlight-comment-bg); + color: var(--highlight-text-color); + box-sizing: border-box; + + margin: 2px; + padding: 1em; + font-size: 0.75em; + max-width: 400px; + border: solid 1px transparent; + + @media screen and (max-width: 985px) { + max-width: 100%; + margin-left: 0; + margin-right: 0; padding: 1em; - font-size: 0.75em; - max-width: 400px; - border: solid 1px transparent; - - - - @media screen and (max-width: 985px) { - max-width: 100%; - margin-left: 0; - margin-right: 0; - padding: 1em; - /* display: none; */ - - &.text-highlight-selected { - display: block; - - /*Got help from this video: https://www.youtube.com/watch?v=vmDEHAzj2XE */ - transition-property: display; - transition-duration: 1s; - transition-behavior: allow-discrete; - animation: 0.1s ease-out 0s 1 slideInFromBottom; + /* display: none; */ - } + &.text-highlight-selected { + display: block; - &:not(.text-highlight-selected) { - animation: 0.1s ease-out 0s 1 slideOutToBottom; - display: none; - transition-property: display; - transition-duration: 0.1s; - transition-behavior: allow-discrete; - } + /*Got help from this video: https://www.youtube.com/watch?v=vmDEHAzj2XE */ + transition-property: display; + transition-duration: 1s; + transition-behavior: allow-discrete; + animation: 0.1s ease-out 0s 1 slideInFromBottom; + } + &:not(.text-highlight-selected) { + animation: 0.1s ease-out 0s 1 slideOutToBottom; + display: none; + transition-property: display; + transition-duration: 0.1s; + transition-behavior: allow-discrete; } + } } - .text-highlight-hover { - border-color: var(--highlight-border-hover); - box-shadow: 1px 1px 2px var(--highlight-comment-box-shadow); - transition: box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; - cursor: pointer; + border-color: var(--highlight-border-hover); + box-shadow: 1px 1px 2px var(--highlight-comment-box-shadow); + transition: box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out; + cursor: pointer; } .text-highlight-selected { - border: solid 1px var(--highlight-border-selected); + border: solid 1px var(--highlight-border-selected); } - - @keyframes slideInFromBottom { - 0% { - transform: translateY(100%); - } + 0% { + transform: translateY(100%); + } - 100% { - transform: translateY(0); - } + 100% { + transform: translateY(0); + } } @keyframes slideOutToBottom { - 0% { - transform: translateY(0); - } + 0% { + transform: translateY(0); + } - 100% { - transform: translateY(100%); - } + 100% { + transform: translateY(100%); + } } - -.text-highlight-comment>button.rth-close-button { - display: none; - margin: 2px 2px 0 auto; - - background: none; - background-color: var(--highlight-comment-bg); - border: 1px solid var(--highlight-border-hover); - border-radius: 4px; - /* fully rounded edges */ - padding: 0.5em 1em; - color: var(--highlight-text-color); - font: inherit; - font-size: 0.75em; - cursor: pointer; - outline: none; - transition: background 0.2s, color 0.2s; - - - @media screen and (max-width: 985px) { - display: block; - } - - -} \ No newline at end of file +.text-highlight-comment > button.rth-close-button { + display: none; + margin: 2px 2px 0 auto; + + background: none; + background-color: var(--highlight-comment-bg); + border: 1px solid var(--highlight-border-hover); + border-radius: 4px; + /* fully rounded edges */ + padding: 0.5em 1em; + color: var(--highlight-text-color); + font: inherit; + font-size: 0.75em; + cursor: pointer; + outline: none; + transition: background 0.2s, color 0.2s; + + @media screen and (max-width: 985px) { + display: block; + } +} diff --git a/src/components/CodeExampleLink/CodeExampleLink.tsx b/src/components/CodeExampleLink/CodeExampleLink.tsx index f2e10bd9..aba858a0 100644 --- a/src/components/CodeExampleLink/CodeExampleLink.tsx +++ b/src/components/CodeExampleLink/CodeExampleLink.tsx @@ -1,6 +1,10 @@ import React from "react"; -export function CodeExampleLink(props: {link: string, text?: string}) { - const {link, text} = props; - return Code Example: {text ?? link} -} \ No newline at end of file +export function CodeExampleLink(props: { link: string; text?: string }) { + const { link, text } = props; + return ( + + Code Example: {text ?? link} + + ); +} diff --git a/src/components/DatePublished/DatePublished.tsx b/src/components/DatePublished/DatePublished.tsx index 7044febf..28d526d5 100644 --- a/src/components/DatePublished/DatePublished.tsx +++ b/src/components/DatePublished/DatePublished.tsx @@ -1,10 +1,21 @@ +export function DatePublished(props: { + date: string | Date; + className?: string; +}) { + const { className = "", date } = props; -export function DatePublished(props: { date: string | Date, className?: string}) { - - const {className = '', date} = props; - - return
    - Published: + return ( +
    + + Published: + +
    - -} \ No newline at end of file + ); +} diff --git a/src/components/DemoFrame/DemoFrame.tsx b/src/components/DemoFrame/DemoFrame.tsx index 5097791e..deefd5ce 100644 --- a/src/components/DemoFrame/DemoFrame.tsx +++ b/src/components/DemoFrame/DemoFrame.tsx @@ -1,17 +1,17 @@ -import { PropsWithChildren, ReactNode } from "react"; +import type { PropsWithChildren, ReactNode } from "react"; export type DemoFrameProps = PropsWithChildren<{ - description?: string | ReactNode; + description?: string | ReactNode; }>; export function DemoFrame(props: DemoFrameProps) { - - - return
    -

    ☝️ Interactive demo

    -
    - {props.children} -
    -
    {props.description}
    + return ( +
    +

    ☝️ Interactive demo

    +
    {props.children}
    +
    + {props.description} +
    -} \ No newline at end of file + ); +} diff --git a/src/components/EditWithGithub/EditWithGithub.tsx b/src/components/EditWithGithub/EditWithGithub.tsx index dfa6ecfe..8d36ff34 100644 --- a/src/components/EditWithGithub/EditWithGithub.tsx +++ b/src/components/EditWithGithub/EditWithGithub.tsx @@ -1,23 +1,23 @@ - -import React from 'react'; +import React from "react"; export type EditWithGithubProps = { - postName: string; + postName: string; }; - - export const EditWithGithub = (props: EditWithGithubProps) => { - const { postName } = props; - - - - return ( -
    -
    -

    - Spotted an error? Edit this page with Github -

    -
    - ); + const { postName } = props; + + return ( +
    +
    +

    + Spotted an error?{" "} + + Edit this page with Github + +

    +
    + ); }; diff --git a/src/components/FrontmatterBox/FrontmatterBox.tsx b/src/components/FrontmatterBox/FrontmatterBox.tsx index 025527f2..d0091ec3 100644 --- a/src/components/FrontmatterBox/FrontmatterBox.tsx +++ b/src/components/FrontmatterBox/FrontmatterBox.tsx @@ -1,93 +1,111 @@ +import { getFrontmatterFromSlug } from "@/utils/blogPosts"; +import Link from "next/link"; import type { PropsWithChildren } from "react"; import React from "react"; -import { getFrontmatterFromSlug } from "@/utils/blogPosts"; import type { EnrichedFrontMatterPlusSlug } from "../../../utils/frontmatterTypings"; -import Link from "next/link"; import { DatePublished } from "../DatePublished/DatePublished"; type FrontmatterBoxProps = { - frontmatter: EnrichedFrontMatterPlusSlug | null; -} - + frontmatter: EnrichedFrontMatterPlusSlug | null; +}; function SeriesBox(props: FrontmatterBoxProps) { + if (!props.frontmatter?.seriesFrontmatter) { + return null; + } - if (!props.frontmatter?.seriesFrontmatter) { - return null; - } - - if (!props.frontmatter.frontmatter.series) { - return null; - } - - const firstSeriesItem = props.frontmatter?.seriesFrontmatter[0]; - - return
    -

    - This article is a part of the series "{firstSeriesItem.frontmatter.series?.description ?? firstSeriesItem.frontmatter.series?.name}"

    -
      - {props.frontmatter?.seriesFrontmatter?.map((v) => { - return
    • - - {v.frontmatter.meta?.title ?? v.slug} - -
    • - })} -
    + if (!props.frontmatter.frontmatter.series) { + return null; + } + + const firstSeriesItem = props.frontmatter?.seriesFrontmatter[0]; + + return ( +
    +

    + This article is a part of the series " + + {firstSeriesItem.frontmatter.series?.description ?? + firstSeriesItem.frontmatter.series?.name} + + "{" "} +

    +
      + {props.frontmatter?.seriesFrontmatter?.map((v) => { + return ( +
    • + + {v.frontmatter.meta?.title ?? v.slug} + +
    • + ); + })} +
    - + ); } - function partIsNumber(part: number | undefined): part is number { - return typeof part === 'number'; + return typeof part === "number"; } function NextBox(props: FrontmatterBoxProps) { - const part = props.frontmatter?.frontmatter.series?.part; - if (!partIsNumber(part)) { - return null; - } - - const nextInSeries = part; // nb. the series are 1 indexed, but the array here is 0 indexed. - - if (props.frontmatter?.seriesFrontmatter && props.frontmatter.seriesFrontmatter[nextInSeries]) { - - const nextPost = props.frontmatter.seriesFrontmatter[nextInSeries]; - - return
    - Next: {nextPost.frontmatter.meta.title} -
    - } + const part = props.frontmatter?.frontmatter.series?.part; + if (!partIsNumber(part)) { return null; + } + + const nextInSeries = part; // nb. the series are 1 indexed, but the array here is 0 indexed. + + if ( + props.frontmatter?.seriesFrontmatter && + props.frontmatter.seriesFrontmatter[nextInSeries] + ) { + const nextPost = props.frontmatter.seriesFrontmatter[nextInSeries]; + + return ( +
    + + Next: {nextPost.frontmatter.meta.title} + +
    + ); + } + return null; } - -export async function FrontmatterBox(props: PropsWithChildren<{ - slug: string -}>) { - - const frontmatter = await getFrontmatterFromSlug(props.slug); - - if (!frontmatter) { - return <>{props.children}; - } - - return <> -
    - -
    -
    -

    {frontmatter.frontmatter.meta.title}

    - -
    - {props.children} - - - <> -
    -
    - Questions? Comments? Criticisms? Get in the comments! 👇 - +export async function FrontmatterBox( + props: PropsWithChildren<{ + slug: string; + }> +) { + const frontmatter = await getFrontmatterFromSlug(props.slug); + + if (!frontmatter) { + return <>{props.children}; + } + + return ( + <> +
    + +
    +
    +

    {frontmatter.frontmatter.meta.title}

    + +
    + {props.children} + + + <> +
    +
    + Questions? Comments? Criticisms?{" "} + Get in the comments! 👇 + -} \ No newline at end of file + ); +} diff --git a/src/components/ImagePanel/ImagePanel.tsx b/src/components/ImagePanel/ImagePanel.tsx index 583b1c85..a1b5f6a7 100644 --- a/src/components/ImagePanel/ImagePanel.tsx +++ b/src/components/ImagePanel/ImagePanel.tsx @@ -1,25 +1,25 @@ -import { ReactNode } from "react" +import type { ReactNode } from "react"; type ImageWithCaption = { - image: ReactNode, - caption?: string; -} + image: ReactNode; + caption?: string; +}; export function ImagePanel(props: { - images: ImageWithCaption | Array; + images: ImageWithCaption | Array; }) { + const images = Array.isArray(props.images) ? props.images : [props.images]; - const images = Array.isArray(props.images) ? props.images : [props.images]; - - - - return
    - {images.map((v,i) => { - return
    - {v.image} -

    {v.caption}

    -
    - })} - + return ( +
    + {images.map((v, i) => { + return ( +
    + {v.image} +

    {v.caption}

    +
    + ); + })}
    -} \ No newline at end of file + ); +} diff --git a/src/components/InfoPanel/InfoPanel.tsx b/src/components/InfoPanel/InfoPanel.tsx index 5bdeba23..1cf4e54f 100644 --- a/src/components/InfoPanel/InfoPanel.tsx +++ b/src/components/InfoPanel/InfoPanel.tsx @@ -1,22 +1,26 @@ import type { PropsWithChildren } from "react"; type InfoPanelProps = PropsWithChildren<{ - level: "warning" | "info" | "instruction"; - className?: string; -}> + level: "warning" | "info" | "instruction"; + className?: string; +}>; export function InfoPanel(props: InfoPanelProps) { + const { level, className = "", children } = props; - const { level, className = '', children } = props; + return ( +
    + {level === "warning" && ( + warning + )} + {level === "info" && ( + info + )} + {level === "instruction" && ( + lightbulb + )} - return
    - {level === "warning" && warning} - {level === "info" && info} - {level === "instruction" && lightbulb} - -
    - {children} -
    +
    {children}
    - -} \ No newline at end of file + ); +} diff --git a/src/components/ListOfArticles/ListOfArticles.tsx b/src/components/ListOfArticles/ListOfArticles.tsx index 57654d25..edaa7c69 100644 --- a/src/components/ListOfArticles/ListOfArticles.tsx +++ b/src/components/ListOfArticles/ListOfArticles.tsx @@ -1,40 +1,72 @@ -import { FrontMatterPlusSlug } from "../../../utils/frontmatterTypings"; -import { DatePublished } from "../DatePublished/DatePublished" -import Link from "next/link" - +import Link from "next/link"; +import type { FrontMatterPlusSlug } from "../../../utils/frontmatterTypings"; +import { DatePublished } from "../DatePublished/DatePublished"; function FilterInformation(props: { filterInformation: { - tagFilter: string | null, - didFindArticles: boolean - } + tagFilter: string | null; + didFindArticles: boolean; + }; }) { - return
    - {props.filterInformation.tagFilter && props.filterInformation.didFindArticles &&

    Articles tagged "{props.filterInformation.tagFilter}":

    } - {props.filterInformation.tagFilter && !props.filterInformation.didFindArticles && <>

    No articles found for tag "{props.filterInformation.tagFilter}"

    You might find these articles useful:

    } -
    + return ( +
    + {props.filterInformation.tagFilter && + props.filterInformation.didFindArticles && ( +

    + Articles tagged{" "} + "{props.filterInformation.tagFilter}": +

    + )} + {props.filterInformation.tagFilter && + !props.filterInformation.didFindArticles && ( + <> +

    + No articles found for tag{" "} + "{props.filterInformation.tagFilter}" +

    +

    You might find these articles useful:

    + + )} +
    + ); } -export function ListOfArticles(props: { - allFrontmatter: Array, - filterInformation?: { - tagFilter: string | null, - didFindArticles: boolean - } - }) { - return
    - - {props.filterInformation && } +export function ListOfArticles(props: { + allFrontmatter: Array; + filterInformation?: { + tagFilter: string | null; + didFindArticles: boolean; + }; +}) { + return ( +
    + {props.filterInformation && ( + + )} {props.allFrontmatter.map((v) => { - return -
    -

    {v.frontmatter.meta?.title ?? v.slug}

    - {v.frontmatter.meta?.dateCreated && } -

    {v.frontmatter.meta?.description ?? ''}

    -
    - + return ( + +
    +

    + {v.frontmatter.meta?.title ?? v.slug} +

    + {v.frontmatter.meta?.dateCreated && ( + + )} +

    + {v.frontmatter.meta?.description ?? ""} +

    +
    + + ); })} -
    - } \ No newline at end of file + ); +} diff --git a/src/components/ListOfTagsPanel/ListOfTagsPanel.tsx b/src/components/ListOfTagsPanel/ListOfTagsPanel.tsx index dd88b3f2..87729ba4 100644 --- a/src/components/ListOfTagsPanel/ListOfTagsPanel.tsx +++ b/src/components/ListOfTagsPanel/ListOfTagsPanel.tsx @@ -1,48 +1,55 @@ import Link from "next/link"; import tags from "../../generated/tags.json"; - -console.log(Object.entries(tags)) +console.log(Object.entries(tags)); // Sort by the number of posts in each tag -const uniqueTags = Object.entries(tags).toSorted((a, b) => { - - +const uniqueTags = Object.entries(tags) + .toSorted((a, b) => { // This is a bit hacky, but basically because all the test/draft posts are untagged, they increase the count. // We just make the untagged posts always be at the bottom if (a[0] === "untagged") { - return 1; + return 1; } if (b[0] === "untagged") { - return -1; + return -1; } return b[1].length - a[1].length; -}).map((v) => { + }) + .map((v) => { return v[0]; -}); + }); const tagToLabelMap = { - "react": "React", - "nextjs": "Next.js", - "testing": "Testing", - "typescript": "TypeScript", - "javascript_nitty_gritty": "JavaScript", - "developer_experience": "Developer Experience", - "openapi": "OpenAPI", - "software_engineering": "Software Engineering", - "infrastructure": "Infrastructure", - "untagged": "Untagged", - "blogging": "Blogging", - "git": "Git" + react: "React", + nextjs: "Next.js", + testing: "Testing", + typescript: "TypeScript", + javascript_nitty_gritty: "JavaScript", + developer_experience: "Developer Experience", + openapi: "OpenAPI", + software_engineering: "Software Engineering", + infrastructure: "Infrastructure", + untagged: "Untagged", + blogging: "Blogging", + git: "Git", } as Record; export function ListOfTagsPanel() { - return