blog: vibe check #21

This commit is contained in:
Thomas 2025-01-09 14:08:29 +00:00
parent 1743c9cfd4
commit 98bc188722
No known key found for this signature in database
13 changed files with 160 additions and 65 deletions

View file

@ -32,7 +32,7 @@
"svelte-preprocess": "^6.0.0", "svelte-preprocess": "^6.0.0",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.11", "vite": "^6.0.7",
"vitest": "^1.6.0" "vitest": "^1.6.0"
}, },
"type": "module", "type": "module",
@ -60,6 +60,6 @@
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"engines": { "engines": {
"node": ">= 20.11.1" "node": ">=22.0.0"
} }
} }

View file

@ -0,0 +1,27 @@
---
title: 'Vibe Check #21'
date: 1970-01-01T00:00:00.000Z
slug: 2025-01-09-vibe-check-21
author: Thomas Wilson
---
![](https://www.herearethose.photos/api/files/76032892-8c71-4a69-80a9-cee58525b6bd/cdn?viewport=lg)
December comes to us, mild in temperament and temperature this year. Although this season didn't see quite the Grand Social Obligations Tour that previous festive periods have, the household has remained busy (and largely healthy) through December, something to always be celebrated.
The month contained moments of great personal and social delight which, though both real and invaluable, aren't fit to share far and wide on the open internet. Let the record show that I feel both grateful and fulfilled by the people in my life, and my relationships to them.
December 2024 saw my last working day at my (now previous) job as a Senior Software Engineer. In a few weeks I'll be starting a more leadership-focused position in a larger organisation, with a much larger technical footprint. I am excited, and glad to have taken a short (three-week) break between positions. Time off has a funny way of being neither as much time, nor off, as one expects at the outset.
A few months before the end of 2024 I wrote a list of projects I had started, and wanted to finish. These were things I *wanted* to finish but which had started dragging. I am proud to report that in the last four weeks of 2024, I managed to:
1. Finish knitting my first pair of socks
2. Finish the second book in John Gwynne's *Bloodsword Saga*
I continue to make a lot of my own clothes. My relationship to this practice *is* getting more complicated. I have a lot of thoughts about how inaccessible the knowledge of "proper" tailoring and garment construction is. I wonder if that's a "real" problem, or only a "me" problem. I am infuriated by how often "you just have to know" is the answer to questions, and I wonder how much [Cargo Culting](https://en.wikipedia.org/wiki/Cargo_cult) or Rain Dancing (i.e. rituals seem to have previously worked, but through unclear mechanisms) happens here.
But I am delighted to report that knitting has little to none of this. My experience has been that people are *delighted* to provide you with very clear, standardised instructions on how to make a pair of socks. Even better, similar services are available for many other garments! I am also currently batting 100% on "unclear instructions that an LLM can help clarify". It definitely takes *longer* to make knitted garments - *weeks* for two socks - and one cannot practically wear only knitted cloths, but it is a skill I am grateful to have, and to improve.
I wanted the pressure to finish the garment precisely for this reason: it is refreshing and I am glad to have done it, but I found it hard to assign time to it.
The book was a similar story: a recently-completed trilogy that I am actively enjoying. But at 600-some pages, and as a physical book (I am 1:2 on books:audiobooks) I found myself picking up, and finishing, books around it. By setting the end-of-year marker, I was able to leave that feeling of drag behind. And it feels fantastic.

View file

@ -1,9 +1,10 @@
import { resolve } from 'path'; import { resolve } from 'path';
import { json, error } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { BlogController } from '../../../../lib/blog/BlogController.js';
import { dump as dumpYaml } from 'js-yaml'; import { dump as dumpYaml } from 'js-yaml';
import { BlogController } from '../../../../lib/blog/BlogController.js';
const thisDirectory = import.meta.url const thisDirectory = import.meta.url
.replace('file://', '') .replace('file://', '')
.split('/') .split('/')
@ -11,6 +12,7 @@ const thisDirectory = import.meta.url
.join('/'); .join('/');
export const POST: RequestHandler = async ({ getClientAddress, request }) => { export const POST: RequestHandler = async ({ getClientAddress, request }) => {
console.log(`Received request to create new blog post.`);
const address = await getClientAddress(); const address = await getClientAddress();
let fileName: string; let fileName: string;
let markdownContent: string; let markdownContent: string;
@ -33,6 +35,15 @@ export const POST: RequestHandler = async ({ getClientAddress, request }) => {
error(400, 'Error in request body.'); error(400, 'Error in request body.');
} }
console.log({
fileName,
markdownContent,
title,
date,
slug,
author,
});
if ([fileName, markdownContent, title, date, slug, author].includes(undefined)) { if ([fileName, markdownContent, title, date, slug, author].includes(undefined)) {
error(400, `Missing parameters.`); error(400, `Missing parameters.`);
} else if (!['127.0.0.1', '::1'].includes(address)) { } else if (!['127.0.0.1', '::1'].includes(address)) {

View file

@ -1 +1,66 @@
import { BlogController } from '$lib/blog/BlogController.js';
import type { Actions } from '@sveltejs/kit';
import { error, redirect } from '@sveltejs/kit';
import { dump as dumpYaml } from 'js-yaml';
import { resolve } from 'path';
const thisDirectory = import.meta.url
.replace('file://', '')
.split('/')
.filter((part) => part !== '+server.ts')
.join('/');
export const prerender = true; export const prerender = true;
export const actions = {
default: async ({ getClientAddress, request }) => {
console.log(`Received request to create new blog post.`);
const address = await getClientAddress();
let markdownContent: string;
let title: string;
let date: string;
let slug: string;
let author: string;
try {
const requestBody = await request.formData();
markdownContent = requestBody.get('content') as string;
title = requestBody.get('title') as string;
date = requestBody.get('date') as string;
slug = requestBody.get('slug') as string;
author = requestBody.get('author') as string;
} catch (e: any) {
console.log(`Caught error destructuring request body`);
console.error(e);
error(400, 'Error in request body.');
}
if ([markdownContent, title, date, slug, author].includes(undefined)) {
error(400, `Missing parameters.`);
} else if (!['127.0.0.1', '::1'].includes(address)) {
console.log(address);
error(403, `Forbidden.`);
}
const controller = await BlogController.singleton();
const worryinglyManualFrontMatter = [
`---`,
dumpYaml({ title, date: new Date(date), slug, author }),
`---`,
].join(`\n`);
const escapedMarkdown = markdownContent.replaceAll(/\\n/g, '\n');
const contentWithFrontmatter = [worryinglyManualFrontMatter, escapedMarkdown].join(`\n`);
const resolvedFileName = resolve(thisDirectory, `../../../content/blog/${slug}.md`);
console.log({ resolvedFileName });
console.log(`\n${contentWithFrontmatter}\n`);
await controller.createBlogPost(resolvedFileName, contentWithFrontmatter);
redirect(307, `/blog/${slug}`);
},
} satisfies Actions;

View file

@ -86,6 +86,7 @@
text-align: center; text-align: center;
line-height: 125%; line-height: 125%;
max-width: 30ch; max-width: 30ch;
font-family: var(--font-family-serif);
} }
.post-author { .post-author {
@ -93,6 +94,7 @@
text-align: center; text-align: center;
line-height: 100%; line-height: 100%;
margin: 0; margin: 0;
font-weight: 300;
} }
.post-date { .post-date {
@ -129,22 +131,22 @@
#article { #article {
--padding: 4px; --padding: 4px;
background-color: var(--colour-scheme-background-accent); --font-size: var(--font-size-base);
padding: var(--padding); padding: var(--padding);
font-size: var(--font-size-base); font-size: var(--font-size);
width: var(--width); width: var(--width);
max-width: var(--max-width); max-width: var(--max-width);
flex-grow: 1; flex-grow: 1;
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
--font-size: 1rem; --font-size: var(--font-size-sm);
--padding: 4px;
letter-spacing: normal; letter-spacing: normal;
line-height: 110%; line-height: 110%;
--padding: 4px;
} }
@media screen and (min-width: 700px) { @media screen and (min-width: 700px) {
--font-size: 1.29rem; --font-size: var(--font-size-base);
--padding: 1.5rem 2rem; --padding: 1.5rem 2rem;
} }
} }

View file

@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { format as formatDate } from "date-fns"; import { format as formatDate } from "date-fns";
import { BlogPost } from "$lib/blog/BlogPost.js"; import { BlogPost } from "$lib/blog/BlogPost.js";
import { goto } from "$app/navigation";
let title = $state(""); let title = $state("");
let author = $state("Thomas Wilson"); let author = $state("Thomas Wilson");
let date = new Date(); let date = new Date();
@ -24,42 +21,18 @@
const slugifiedTitle = slugifyString(title); const slugifiedTitle = slugifyString(title);
slug = `${dateAsString}-${slugifiedTitle}`; slug = `${dateAsString}-${slugifiedTitle}`;
} }
async function onCreate() {
const requestBody = {
title,
author,
slug,
markdownContent: content,
fileName: `${slug}.md`,
date: date.toISOString(),
};
fetch("/api/blog/new.json", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
}).then(async (res) => {
if (res.status === 200) {
await goto(`/blog/${slug}`);
} else {
alert("Something went wrong");
}
});
}
</script> </script>
<section class="new-blog-post"> <section class="new-blog-post">
<a href="/blog">Back to Blog</a> <a href="/blog">Back to Blog</a>
<h1>New Blog Post</h1> <h1>New Blog Post</h1>
<form onsubmit={preventDefault(onCreate)}> <form method="POST" action="/blog">
<div class="field"> <div class="field">
<label class="field__label" for="title">Title</label> <label class="field__label" for="title">Title</label>
<input <input
type="text" type="text"
id="title" id="title"
name="title"
required required
bind:value={title} bind:value={title}
onchange={handleTitleChange} onchange={handleTitleChange}
@ -67,21 +40,33 @@
</div> </div>
<div class="field"> <div class="field">
<label class="field__label" for="author">Author</label> <label class="field__label" for="author">Author</label>
<input type="text" id="author" required bind:value={author} /> <input
type="text"
name="author"
id="author"
required
bind:value={author}
/>
</div> </div>
<div class="field"> <div class="field">
<label class="field__label" for="slug">Slug</label> <label class="field__label" for="slug">Slug</label>
<input type="text" id="slug" required bind:value={slug} /> <input type="text" name="slug" id="slug" required bind:value={slug} />
</div> </div>
<div class="field"> <div class="field">
<label class="field__label" for="content">Content</label> <label class="field__label" for="content">Content</label>
<textarea id="content" rows="10" cols="50" bind:value={content}></textarea> <textarea
name="content"
id="content"
rows="10"
cols="50"
bind:value={content}
></textarea>
</div> </div>
<div class="submit"> <div class="submit">
<button class="create-button">Publish</button> <button type="submit" class="create-button">Publish</button>
</div> </div>
</form> </form>
</section> </section>

View file

@ -20,9 +20,9 @@
I craft well-meaning things, like software, clothes, photographs, and words. I craft well-meaning things, like software, clothes, photographs, and words.
</p> </p>
<p class="body strapline"> <p class="body strapline">
I am trying to make things stronger which don&apos;t rely on exploitation, I want to help build things that don&apos;t rely on exploitation, excessive
excessive waste, or dishonesty; and to try and build a kinder, fairer world waste, or dishonesty; and to try and build a kinder, fairer world even when
even when it's hard. it's hard.
</p> </p>
<hr /> <hr />

View file

@ -22,7 +22,7 @@
totalNumberOfGuesses: 0, totalNumberOfGuesses: 0,
guesses: [], guesses: [],
correctDays: [], correctDays: [],
incorrectDays: [] incorrectDays: [],
}); });
interface Props { interface Props {
@ -47,7 +47,7 @@
totalNumberOfGuesses: 0, totalNumberOfGuesses: 0,
guesses: [], guesses: [],
correctDays: [], correctDays: [],
incorrectDays: [] incorrectDays: [],
}); });
} }
@ -69,13 +69,13 @@
revealNotification(true); revealNotification(true);
$guessingHistory.correctDays = [ $guessingHistory.correctDays = [
...$guessingHistory.correctDays, ...$guessingHistory.correctDays,
todaysDateString todaysDateString,
]; ];
} else { } else {
revealNotification(false); revealNotification(false);
$guessingHistory.incorrectDays = [ $guessingHistory.incorrectDays = [
...$guessingHistory.incorrectDays, ...$guessingHistory.incorrectDays,
todaysDateString todaysDateString,
]; ];
} }
} }
@ -152,7 +152,7 @@
{/if} {/if}
</div> </div>
<style lang="scss" type="text/postcss"> <style>
:root { :root {
--background-colour: hsl(40, 8%, 85%); --background-colour: hsl(40, 8%, 85%);
--colour-light-grey: hsl(28, 35%, 85%); --colour-light-grey: hsl(28, 35%, 85%);

View file

@ -14,7 +14,7 @@
const resultSpring = spring(20, { const resultSpring = spring(20, {
stiffness: 0.1, stiffness: 0.1,
damping: 0.15, damping: 0.15,
precision: 0.01 precision: 0.01,
}); });
function triggerAnimation() { function triggerAnimation() {
@ -58,7 +58,7 @@
{/if} {/if}
</section> </section>
<style type="text/postcss"> <style>
.notification { .notification {
} }

View file

@ -38,7 +38,7 @@
{/if} {/if}
</section> </section>
<style type="text/postcss"> <style>
.options { .options {
padding: 12px; padding: 12px;
display: grid; display: grid;

View file

@ -13,18 +13,20 @@
doesUserHaveGuessingHistory, doesUserHaveGuessingHistory,
correctGuessDays, correctGuessDays,
incorrectGuessDays, incorrectGuessDays,
currentStreakLength currentStreakLength,
}: Props = $props(); }: Props = $props();
const todayAsString = formatDate(new Date(), "yyyy-MM-dd"); const todayAsString = formatDate(new Date(), "yyyy-MM-dd");
const calculator = new SunriseSunsetStreakCalculator(todayAsString); const calculator = new SunriseSunsetStreakCalculator(todayAsString);
let hasTextBeenCopied = $state(false); let hasTextBeenCopied = $state(false);
let historyStatement = $derived(calculator.getShareableStatement( let historyStatement = $derived(
calculator.getShareableStatement(
correctGuessDays, correctGuessDays,
incorrectGuessDays, incorrectGuessDays,
new Date() new Date()
)); )
);
function copyHistory() { function copyHistory() {
if (browser) { if (browser) {
@ -52,7 +54,7 @@
</div> </div>
</section> </section>
<style type="text/postcss"> <style>
.score { .score {
display: flex; display: flex;
place-content: center; place-content: center;
@ -71,7 +73,9 @@
padding: 12px; padding: 12px;
border-radius: 4px; border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.1),
0 1px 2px rgba(0, 0, 0, 0.1);
} }
.score__title { .score__title {

View file

@ -32,8 +32,8 @@
--line-height-md: 140%; --line-height-md: 140%;
--line-height-lg: 145%; --line-height-lg: 145%;
--font-size-xs: 0.75rem; --font-size-xs: 11px;
--font-size-sm: 0.875rem; --font-size-sm: 13px;
--font-size-base: 1rem; --font-size-base: 1rem;
--font-size-lg: 1.25rem; --font-size-lg: 1.25rem;
--font-size-xl: 1.5rem; --font-size-xl: 1.5rem;

View file

@ -8,7 +8,8 @@ const config = {
extensions: ['.svelte', '.md'], extensions: ['.svelte', '.md'],
// Consult https://github.com/sveltejs/svelte-preprocess // Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors // for more information about preprocessors
preprocess: [preprocess(), mdsvex({ preprocess: [preprocess(),
mdsvex({
extensions: ['.md'], extensions: ['.md'],
})], })],