This commit is contained in:
Thomas 2024-03-17 09:33:22 +00:00
parent f6577d5018
commit 4c98640a08
12 changed files with 232 additions and 285 deletions

View file

@ -60,7 +60,11 @@ export class BlogController {
return createdBlogPost; return createdBlogPost;
} }
async getAllBlogPosts(): Promise<Array<BlogPostListItem | BookReviewListItem | SnoutStreetStudiosPostListItem>> { async getAllBlogPosts(
pageSize?: number
): Promise<Array<BlogPostListItem | BookReviewListItem | SnoutStreetStudiosPostListItem>> {
console.log('getAllBlogPosts')
console.log({ pageSize });
const blogPosts = await this._markdownRepository.blogPosts; const blogPosts = await this._markdownRepository.blogPosts;
const bookReviews = await this._markdownRepository.bookReviews; const bookReviews = await this._markdownRepository.bookReviews;
@ -79,9 +83,16 @@ export class BlogController {
(post) => this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(post) (post) => this.snoutStreetStudiosPostToSnoutStreetStudiosPostListItem(post)
); );
return [...blogPostListItems, ...bookReviewListItems, ...snoutStreetStudiosPostListItems].sort((a, b) => const allBlogPosts = [...blogPostListItems, ...bookReviewListItems, ...snoutStreetStudiosPostListItems].sort((a, b) =>
a.date > b.date ? -1 : 1 a.date > b.date ? -1 : 1
); )
if (pageSize === undefined) {
console.log('returning all blog posts')
return allBlogPosts;
}
return allBlogPosts.slice(0, pageSize);
} }
private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem { private bookReviewToBookReviewListItem(bookReview: BookReview): BookReviewListItem {

View file

@ -1,10 +1,23 @@
<script lang="ts">
import { colourSchemeStore, lightColourScheme, darkColourScheme } from "../../stores/colourSchemeStore";
function onColourSchemeChange() {
if ($colourSchemeStore.name === 'dark') {
colourSchemeStore.set(lightColourScheme)
} else {
colourSchemeStore.set(darkColourScheme)
}
}
</script>
<nav> <nav>
<div class="left"> <div class="left">
<a href="/" class="home">Thomas Wilson</a> <a href="/" class="home">Thomas Wilson</a>
</div> </div>
<div class="right"> <div class="right">
<a href="/blog" class="blog">Blog</a> <button class="colour-theme-toggle" on:click={onColourSchemeChange}>Toggle Colour Scheme</button>
<a href="/blog" class="blog">/blog</a>
</div> </div>
</nav> </nav>
@ -15,6 +28,7 @@
max-width: 100vw; max-width: 100vw;
overflow: hidden; overflow: hidden;
min-height: var(--navbar-height); min-height: var(--navbar-height);
font-family: var(--font-family-mono);
} }
.left { .left {
@ -22,10 +36,12 @@
flex-grow: 0; flex-grow: 0;
text-align: left; text-align: left;
padding: var(--spacing-base); padding: var(--spacing-base);
} }
.home { .home {
color: var(--brand-orange); color: var(--brand-orange);
font-family: inherit;
text-decoration: none; text-decoration: none;
font-weight: 300; font-weight: 300;
display: flex; display: flex;
@ -42,17 +58,25 @@
display: flex; display: flex;
flex: 1; flex: 1;
text-align: right; text-align: right;
gap: 1rem;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
padding: var(--spacing-base); padding: var(--spacing-base);
} }
.blog { .colour-theme-toggle {
font-size: 1.4rem; display: flex;
padding: 0; align-items: center;
color: var(--gray-1000); justify-content: center;
} }
.blog {
font-size: 1.1rem;
padding: 0;
margin: 0;
}
.blog:visited { .blog:visited {
color: var(--gray-1000); color: var(--colour-scheme-text);
} }
</style> </style>

View file

@ -1,10 +1,36 @@
<script> <script>
import "../styles/thomaswilson.css"; import "../styles/thomaswilson.css";
import { colourSchemeStore } from "../stores/colourSchemeStore";
import { browser } from "$app/environment";
colourSchemeStore.subscribe((value) => {
if (browser) {
document.documentElement.style.setProperty(
"--colour-scheme-background",
value.background
);
document.documentElement.style.setProperty(
"--colour-scheme-text",
value.text
);
document.documentElement.style.setProperty(
"--colour-scheme-background-accent",
value.backgroundAccent
);
document.documentElement.style.setProperty(
"--colour-scheme-text-accent",
value.textAccent
);
}
});
</script> </script>
<svelte:head> <svelte:head>
<title>Thomas Wilson</title> <title>Thomas Wilson</title>
</svelte:head> </svelte:head>
<slot /> <slot />

View file

@ -0,0 +1,16 @@
import { BlogController } from '../lib/blog/BlogController.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
try {
const controller = await BlogController.singleton();
const latestBlogPosts = await controller.getAllBlogPosts(3);
return { latestBlogPosts }
} catch (error) {
console.error({
message: `Caught error in GET /api/blog.json`,
error: JSON.stringify(error),
});
return { latestBlogPosts: [] }
}
};

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { slide } from "svelte/transition";
import Navbar from "$lib/components/Navbar.svelte"; import Navbar from "$lib/components/Navbar.svelte";
import HomepageHeader from "./home/HomepageHeader.svelte";
export let data = { latestBlogPosts: [] }
let isWorkExpanded = false;
let isPersonalExpanded = false;
</script> </script>
<svelte:head> <svelte:head>
@ -12,250 +12,6 @@
<Navbar /> <Navbar />
<main class="thomaswilson-container"> <main class="home">
<section class="section thomaswilson-strapline"> <HomepageHeader latestBlogPosts={data.latestBlogPosts} />
<h1 class="title">Thomas Wilson</h1>
<p>
<span class="strapline-animated" id="statement-1">I am a software engineer who loves the craft of building quality software, </span>
<span class="strapline-animated" id="statement-2">the messy-ness of working with people,</span>
<span class="strapline-animated" id="statement-3">and the balance between <i>minimal</i> and <i>complete</i> products.</span>
<span class="strapline-summary">I build good things with good people for good companies.</span>
</p>
</section>
<section class="work section">
<h2>My work</h2>
{#if isWorkExpanded}
<ul transition:slide="{{ duration: 220 }}">
<li>Right now I am a Senior Software Engineer at <a href="https://www.laka.co.uk" target="_blank" rel="noopener noreferrer">Laka</a>. We're building web tools to un<span class="censor">fuck</span> insurance</li>
<li>
I spent three years building and leading the Software Engienering team at<a
class="oxwash"
target="_blank"
rel="noopener noreferrer"
href="https://www.oxwash.com">Oxwash</a
>, where we built tools for the sustainable and scalable future of laundry.
</li>
<li>
I build full-stack software for the web, especially with TypeScript,
Node, Svelte, and React.
</li>
<li>
I like domain driven design, and agile (with a little 'a')
opinions about delivery and collaboration.
</li>
<li>
I've previously built software with data privacy, ed-tech, and
sustainability companies.
</li>
<li>
I have a <a
class="thesis"
target="_blank"
rel="noopener noreferrer"
href="https://eprints.soton.ac.uk/418168/"
>Ph.D. in education technology</a
>, where I looked at how we can use tech in the teaching lab to
promote and evidence understanding.
</li>
<li>
I have only ever worked with companies actively solving environment
and societal problems.
</li>
<li>
<button on:click="{() => (isWorkExpanded = false)}">
Ew, show less
</button>
</li>
</ul>
{:else}
<p>
I build software that makes complicated things more human-friendly, and
lead teams to do the same.
</p>
<button on:click="{() => (isWorkExpanded = true)}">
Wow, Show more
</button>
{/if}
</section>
<section class="section personal">
<h2>Me</h2>
{#if isPersonalExpanded}
<ul transition:slide="{{ duration: 210 }}">
<li>
I try to write at least once a month on my <a
class="thesis"
href="/blog">blog</a
>.
</li>
<li>I'm learning to sew my own clothes</li>
<li>I love fruity coffees and botanical gins</li>
<li>I used to dance and produce with a contemporary dance company</li>
<li>
<button on:click="{() => (isPersonalExpanded = false)}"
>Cool, show less</button
>
</li>
</ul>
{:else}
<p>
I like to write on my <a href="/blog">blog</a> and ride bikes. I'm learning
to sew my own clothes, and my French is terrible.
</p>
<button on:click="{() => (isPersonalExpanded = true)}"
>Oh, cool, tell me more</button
>
{/if}
</section>
<section class="section links">
<h2>Links</h2>
<ul>
<li>I write here, <a href="/blog">on my blog</a></li>
<li>
And on <a href="https://www.github.com/thomaswilsonxyz">GitHub</a>
</li>
<li>I stopped checking Twitter when... you know.</li>
</ul>
</section>
</main> </main>
<style>
section {
--link-transition: 0.2s;
--link-padding: 4px;
--link-border-radius: 8px;
}
section ul {
margin-top: 0;
margin-bottom: 0;
}
section p {
padding-top: 0;
padding-bottom: var(--spacing-md);
font-size: 1.3rem;
color: var(--gray-700);
}
.thomaswilson-strapline > p {
padding-top: 4px;
padding-bottom: 1rem;
font-size: 1.65rem;
color: var(--gray-700);
}
.oxwash {
padding: var(--link-padding);
transition: var(--link-transition);
border-radius: var(--link-border-radius);
border: 1px solid #0256f2;
color: #0256f2;
text-decoration: none;
}
.oxwash:hover {
background: #6fbbec;
border: 1px solid #6fbbec;
text-decoration: underline;
color: white;
}
.thesis {
padding: var(--link-padding);
transition: var(--link-transition);
border-radius: var(--link-border-radius);
border: 1px solid var(--brand-orange);
color: var(--brand-orange);
text-decoration: none;
}
.thesis:hover {
background: var(--brand-orange);
color: white;
text-decoration: underline;
}
.links a {
color: var(--brand-orange);
}
h1 {
font-size: 2.5rem;
}
h2 {
font-family: var(--font-family-title);
color: var(--gray-800);
}
li {
padding-bottom: 0.3rem;
}
/** Create a class called censor which places a black bar over the text*/
.censor {
position: relative;
color: transparent;
text-shadow: 0 0 3px rgba(0,0,0,0.7);
}
.censor::after {
content: '';
position: absolute;
left: 0;
top: 55%;
width: 100%;
height: 8px;
background-color: black;
transform: translateY(-50%);
}
@keyframes statementAppear {
0% {
opacity: 0.1;
transform: translateY(-20px)
}
100% {
opacity: 1;
transform: translateY(0px)
}
}
.strapline-summary {
font-weight: 400;
}
.strapline-animated {
animation-fill-mode: forwards;
opacity: 0.15;
transform: translateY(-10px)
}
#statement-1 {
animation-name: statementAppear;
animation-delay: 0.5s;
animation-duration: 1.5s;
}
#statement-2 {
animation-name: statementAppear;
animation-delay: 1.4s;
animation-duration: 1.5s;
}
#statement-3 {
animation-name: statementAppear;
animation-delay: 2.3s;
animation-duration: 1.5s;
}
</style>

View file

@ -1 +0,0 @@
export const prerender = false;

View file

@ -3,9 +3,7 @@ import { BlogController } from '../../../lib/blog/BlogController.js';
export const GET = async () => { export const GET = async () => {
try { try {
console.log(`GET /api/blog.json`);
const controller = await BlogController.singleton(); const controller = await BlogController.singleton();
console.log(`Controller instantiated.`);
const blogPosts = await controller.getAllBlogPosts(); const blogPosts = await controller.getAllBlogPosts();
return json({ posts: blogPosts }); return json({ posts: blogPosts });
} catch (error) { } catch (error) {

View file

@ -97,10 +97,6 @@
grid-template-columns: 100%; grid-template-columns: 100%;
gap: var(--spacing-base); gap: var(--spacing-base);
max-width: 100%; max-width: 100%;
@media screen and (min-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
} }
.days-since { .days-since {

View file

@ -1,19 +1,18 @@
<script lang="ts"> <script lang="ts">
import { intlFormat } from "date-fns"; import { format as formatDate } from "date-fns";
export let index: number; export let index: number;
export let numberOfPosts: number; export let numberOfPosts: number;
export let book_review: boolean; export let book_review: boolean;
export let title: string; export let title: string;
export let preview: string; export let preview: string;
export let slug: string; export let slug: string
export let date: string; export let date: string;
export let content_type: "blog" | "book_review" | "snout_street_studios"; export let content_type: "blog" | "book_review" | "snout_street_studios";
$: formattedDate = intlFormat( $: formattedDate = formatDate(
new Date(date), new Date(date),
{ day: "2-digit", month: "long", year: "numeric" }, 'yyyy-mm-dd',
{ locale: "en-GB" }
); );
</script> </script>
@ -58,10 +57,10 @@
.post:hover { .post:hover {
color: var(--brand-orange); color: var(--brand-orange);
background-color: white; background-color: var(--colour-scheme-background-accent);
border: 1px solid var(--brand-orange); border: 1px solid var(--brand-orange);
scale: 1.02; scale: 1.02;
box-shadow: 10px 10px 10px 10px var(--gray-200); box-shadow: 10px 10px 10px 10px rgba(0, 0, 0, 0.5);
} }
.post a { .post a {

View file

@ -0,0 +1,86 @@
<script lang="ts">
import { format as formatDate } from "date-fns";
// TODO: move somewhere common
interface BlogPost {
title: string;
slug: string;
date: string;
}
export let latestBlogPosts: BlogPost[] = [];
</script>
<section class="homepage-header">
<h1 class="title">(Thomas) Wilson</h1>
<p class="body">
I love the craft of well-built things: mostly software (~8 years), but also
clothes (~2 years).
</p>
<p class="body">
I work against systems which rely on exploitation, excessive waste, and
dishonesty. I'm trying to build a kinder and fairer world, and I find that
hard sometimes.
</p>
<p class="body">
I try to think and be curious, that's why I keep a <a href="/blog">weblog</a
>. And (equally embarrassing to say) probably what got me through a
<a
class="thesis"
target="_blank"
rel="noopener noreferrer"
href="https://eprints.soton.ac.uk/418168/"
>Ph.D. in Education Technology</a
>.
</p>
<p class="body">Here are some things I've written recently:</p>
<ol>
{#each latestBlogPosts as post}
<li>
<a href="/blog/{post.slug}">{post.title}</a> ({formatDate(
new Date(post.date),
"yyyy-MM-dd"
)})
</li>
{/each}
</ol>
<p class="body">
Right now I am a Senior Software engineer at <a
href="https://www.laka.co.uk"
target="_blank"
rel="noopener noreferrer">Laka</a
>, building tools actually fairer insurance. Before that, I was Head of
Software Engineering at
<a href="https://www.oxwash.com" target="_blank" rel="noopener noreferrer"
>Oxwash</a
>, building tools for actually sustainable laundry.
</p>
</section>
<style>
.homepage-header {
font-family: monospace;
display: grid;
place-items: center;
}
a,
ol,
li,
p {
color: var(--text-color);
font-family: inherit;
width: 100%;
max-width: 60ch;
}
.title {
font-weight: 400;
font-family: inherit;
}
.body {
text-align: left;
}
</style>

View file

@ -0,0 +1,35 @@
import { writable } from "svelte/store";
type ColourSchemeName = 'light' | 'dark';
interface ColourScheme {
name: ColourSchemeName;
background: string;
backgroundAccent: string;
text: string;
textAccent: string;
}
export const lightColourScheme: ColourScheme = {
name: 'light',
background: 'white',
backgroundAccent: '#f8f9fa',
text: '#212529',
textAccent: '#495057'
};
export const darkColourScheme: ColourScheme = {
name: 'dark',
background: '#212529',
backgroundAccent: '#343a40',
text: '#f8f9fa',
textAccent: '#ced4da'
};
export const colourSchemes: Record<ColourSchemeName, ColourScheme> = {
light: lightColourScheme,
dark: darkColourScheme
};
export const colourSchemeStore = writable<ColourScheme>(darkColourScheme);

View file

@ -21,6 +21,7 @@
--gray-900: #212529; --gray-900: #212529;
--gray-950: #1a1e23; --gray-950: #1a1e23;
--gray-1000: #0a0c0e; --gray-1000: #0a0c0e;
--font-family-mono: monospace;
--font-family-title: 'FivoSansModern-Regular', sans-serif; --font-family-title: 'FivoSansModern-Regular', sans-serif;
--font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
@ -53,9 +54,12 @@
body { body {
font-family: var(--font-family-sans); font-family: var(--font-family-sans);
color: var(--gray-900);
line-height: var(--line-height-md); line-height: var(--line-height-md);
min-height: 100vh; min-height: 100vh;
background-color: var(--colour-scheme-background);
color: var(--colour-scheme-text);
transition: 0.3s ease;
transition-property: background-color, color;
} }
.thomaswilson-container { .thomaswilson-container {
@ -65,7 +69,6 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: calc(100vh - var(--navbar-height) - calc(2 * var(--container-padding))); min-height: calc(100vh - var(--navbar-height) - calc(2 * var(--container-padding)));
background-color: var(--gray-100);
padding: var(--container-padding); padding: var(--container-padding);
} }
@ -76,7 +79,6 @@ body {
font-size: 1.19rem; font-size: 1.19rem;
line-height: var(--line-height-md); line-height: var(--line-height-md);
padding-bottom: var(--spacing-base); padding-bottom: var(--spacing-base);
color: var(--gray-700);
padding-bottom: 2rem; padding-bottom: 2rem;
} }
@ -85,14 +87,13 @@ body {
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
color: var(--gray-800);
} }
.thomaswilson-strapline p { .thomaswilson-strapline p {
font-size: 1.6rem; font-size: 1.4rem;
line-height: var(--line-height-md); line-height: var(--line-height-md);
letter-spacing: -0.25px; letter-spacing: -0.25px;
font-weight: 250; font-weight: 200;
} }
h1, h1,
@ -105,7 +106,7 @@ h6 {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
color: var(--gray-800); color: var(--colour-scheme-text);
padding-top: 8px; padding-top: 8px;
padding-bottom: 6px; padding-bottom: 6px;
line-height: var(--line-height); line-height: var(--line-height);
@ -116,9 +117,9 @@ li,
a { a {
font-size: var(--font-size); font-size: var(--font-size);
line-height: var(--line-height-lg); line-height: var(--line-height-lg);
font-family: var(--font-family-sans); font-family: var(--font-family-mono);
margin: 0; margin: 0;
color: var(--gray-800); color: var(--colour-scheme-text);
padding: 8px 0; padding: 8px 0;
} }