Skip to content

Commit b94ff40

Browse files
author
Paul Armstrong
committed
blog: checkpoint periodization
1 parent 2a60161 commit b94ff40

20 files changed

+1375
-29
lines changed

.astro/types.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,20 @@ declare module 'astro:content' {
322322
collection: "blog",
323323
data: InferEntrySchema<"blog">
324324
} & { render(): Render[".mdx"] },
325+
"2023-07-24-what-makes-a-good-senior-frontend-software-developer.mdx": {
326+
id: "2023-07-24-what-makes-a-good-senior-frontend-software-developer.mdx",
327+
slug: "2023-07-24-what-makes-a-good-senior-frontend-software-developer",
328+
body: string,
329+
collection: "blog",
330+
data: InferEntrySchema<"blog">
331+
} & { render(): Render[".mdx"] },
332+
"2023-07-25-software-development-periodization.mdx": {
333+
id: "2023-07-25-software-development-periodization.mdx",
334+
slug: "2023-07-25-software-development-periodization",
335+
body: string,
336+
collection: "blog",
337+
data: InferEntrySchema<"blog">
338+
} & { render(): Render[".mdx"] },
325339
},
326340

327341
};

astro.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@ import image from '@astrojs/image';
88
import compress from '@otterlord/astro-compress';
99
import react from '@astrojs/react';
1010
import rehypePrettyCode from 'rehype-pretty-code';
11+
import rehypeMermaid from 'rehype-mermaidjs';
1112

1213
const remarkPlugins = [remarkReadingTime];
1314
const rehypePlugins = [
15+
[
16+
rehypeMermaid,
17+
{
18+
strategy: 'inline-svg',
19+
},
20+
],
1421
[
1522
rehypePrettyCode,
1623
{

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"react-component-benchmark": "^2.0.0",
2424
"react-dom": "^18.2.0",
2525
"reading-time": "^1.5.0",
26+
"rehype-mermaidjs": "^1.0.1",
2627
"rehype-pretty-code": "^0.9.5",
2728
"sharp": "^0.32.1",
2829
"solid-js": "^1.7.5",
@@ -46,6 +47,7 @@
4647
"eslint-plugin-tailwindcss": "^3.11.0",
4748
"netlify-cli": "^15.9.0",
4849
"onerepo": "0.11.0",
50+
"playwright": "^1.36.1",
4951
"prettier": "^2.8.8",
5052
"prettier-plugin-astro": "^0.8.0",
5153
"typescript": "^5.0.4"

public/img/blog/contribution.png

152 KB
Loading

src/components/PageInfo.astro

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import Link from '~/components/Link.astro';
33
44
export interface Props {
55
file?: string | undefined;
6-
pubDate?: string | undefined;
7-
updatedDate?: string | undefined;
6+
pubDate?: Date;
7+
updatedDate?: Date | undefined;
88
readingTime?: { minutes: number; words: number } | undefined;
99
}
1010
1111
const { file, pubDate, updatedDate, readingTime } = Astro.props as Props;
12-
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format;
12+
const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeZone: 'UTC' }).format;
1313
---
1414

1515
{
@@ -21,12 +21,12 @@ const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).
2121
<ul class="flex flex-row flex-wrap justify-between gap-4 text-blue-900 dark:text-blue-100">
2222
{pubDate && (
2323
<li>
24-
<b>Published:</b> <time itemprop="datePublished">{dateFormatter(new Date(pubDate))}</time>
24+
<b>Published:</b> <time itemprop="datePublished">{dateFormatter(pubDate)}</time>
2525
</li>
2626
)}
2727
{updatedDate && (
2828
<li>
29-
<b>Last updated:</b> <time itemprop="dateUpdated">{dateFormatter(new Date(updatedDate))}</time>
29+
<b>Last updated:</b> <time itemprop="dateUpdated">{dateFormatter(updatedDate)}</time>
3030
</li>
3131
)}
3232
{readingTime && (

src/components/Snackbar.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { createEffect, createSignal } from 'solid-js';
2-
import type { Component } from 'solid-js';
32

43
interface Props {
54
children: string;
65
}
76

8-
export const Toast: Component<Props> = (props) => {
7+
export function Toast(props: Props) {
98
return (
109
<aside
1110
role="status"
@@ -15,7 +14,7 @@ export const Toast: Component<Props> = (props) => {
1514
<div aria-atomic="false">{props.children}</div>
1615
</aside>
1716
);
18-
};
17+
}
1918

2019
const [toasts, setToasts] = createSignal<Array<string>>([]);
2120

@@ -33,7 +32,7 @@ export function removeToast(text: string) {
3332
});
3433
}
3534

36-
export const Snackbar: Component<void> = () => {
35+
export function Snackbar() {
3736
const [current, setCurrent] = createSignal<string | null>(null);
3837
createEffect(() => {
3938
const visible = toasts()[0];
@@ -55,4 +54,4 @@ export const Snackbar: Component<void> = () => {
5554
{current() ? <Toast>{current()!}</Toast> : null}
5655
</div>
5756
);
58-
};
57+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import clsx from 'clsx';
2+
3+
interface Props {
4+
caption?: string;
5+
contributions: Array<Array<[Date, number, boolean]>>;
6+
typeRange?: [number, number];
7+
}
8+
9+
const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeZone: 'UTC' }).format;
10+
const dayFullFormat = new Intl.DateTimeFormat('en-US', { weekday: 'long', timeZone: 'UTC' }).format;
11+
const dayShortFormat = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: 'UTC' }).format;
12+
const monthFormat = new Intl.DateTimeFormat('en-US', { month: 'short', timeZone: 'UTC' }).format;
13+
const monthFormatFull = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format;
14+
15+
export function ContributionGraph(props: Props) {
16+
const totals = props.contributions[0]!.reduce((memo, [, , inRange], i) => {
17+
memo.push([
18+
props.contributions.reduce((memo, weekday) => {
19+
if (!weekday[i]) {
20+
return memo;
21+
}
22+
return memo + weekday[i]![1];
23+
}, 0),
24+
inRange,
25+
]);
26+
return memo;
27+
}, [] as Array<[number, boolean]>);
28+
const max = Math.max(...totals.map(([t]) => t));
29+
30+
const months = props.contributions[0]!.reduce((memo, [day]) => {
31+
if (day.getMonth() === memo[memo.length - 1]?.date.getMonth()) {
32+
memo[memo.length - 1]!.colSpan += 1;
33+
} else {
34+
memo.push({ date: day, colSpan: 1 });
35+
}
36+
return memo;
37+
}, [] as Array<{ colSpan: number; date: Date }>);
38+
39+
return (
40+
<div class="not-prose overflow-scroll text-[10px] leading-none">
41+
<table class="mb-4 min-w-min max-w-full border-separate border-spacing-[2px]">
42+
{props.caption ? <caption class="text-black dark:text-white">{props.caption}</caption> : null}
43+
<thead>
44+
<tr>
45+
<th></th>
46+
{months.map(({ colSpan, date }) =>
47+
colSpan < 3 ? (
48+
<th colSpan={colSpan} />
49+
) : (
50+
<th role="columnheader" class="text-left text-gray-600 dark:text-gray-400" colSpan={colSpan}>
51+
<span aria-hidden>{monthFormat(date)}</span>
52+
<span class="sr-only">{monthFormatFull(date)}</span>
53+
</th>
54+
)
55+
)}
56+
</tr>
57+
</thead>
58+
<tbody>
59+
{props.contributions.map((dayOfWeek, w) => {
60+
return (
61+
<tr>
62+
<th role="rowheader" class="h-2 w-6 overflow-hidden p-0 pe-1 text-end text-gray-600 dark:text-gray-400">
63+
{!(w % 2) || w === 7 ? null : (
64+
<>
65+
<span class="sr-only">{dayFullFormat(dayOfWeek[0]![0])}</span>
66+
<span aria-hidden>{dayShortFormat(dayOfWeek[0]![0] ?? 0)}</span>
67+
</>
68+
)}
69+
</th>
70+
{dayOfWeek.map(([day, contributions, inRange]) => {
71+
const title = `${contributions} contributions on ${formatter(day)}`;
72+
return (
73+
<td
74+
class={clsx('h-2 w-2 rounded-[0.5px] sm:h-3 sm:w-3', opacity[contributions], getColor(inRange))}
75+
title={title}
76+
>
77+
<div class="h-2 w-2" />
78+
<span class="sr-only">{title}</span>
79+
</td>
80+
);
81+
})}
82+
</tr>
83+
);
84+
})}
85+
</tbody>
86+
</table>
87+
<table class="mb-4 min-w-min max-w-full border-separate border-spacing-[2px]">
88+
<caption class="text-black dark:text-white">Number of contributions by week</caption>
89+
<tr role="presentation">
90+
<td class="w-6 overflow-hidden p-0 pe-1" />
91+
{totals.map(([value, inRange]) => {
92+
return (
93+
<td class="w-2 rounded-[0.5px] p-0 sm:w-3">
94+
<div class="flex h-20 flex-col justify-end">
95+
<div
96+
style={{ 'flex-basis': `${Math.ceil((value / max) * 100)}%` }}
97+
class={clsx(
98+
'w-2 shrink rounded-sm sm:w-3',
99+
opacity[Math.floor((value / max) * 10)],
100+
getColor(inRange)
101+
)}
102+
/>
103+
</div>
104+
</td>
105+
);
106+
})}
107+
</tr>
108+
<tr>
109+
<td class="w-6 overflow-hidden pe-1">
110+
<span class="sr-only">Total</span>
111+
</td>
112+
{totals.map((value) => (
113+
<td class="p-0">
114+
<span class="h-4 shrink-0 text-[9px] leading-none text-black writing-vertical-lr dark:text-white">
115+
{value}
116+
</span>
117+
</td>
118+
))}
119+
</tr>
120+
</table>
121+
</div>
122+
);
123+
}
124+
125+
function weekInRange(week: number, range: [number, number] = [-1, 1000]) {
126+
return !range || (range[0] <= week && range[1] >= week);
127+
}
128+
129+
function getColor(inRange: boolean) {
130+
return inRange ? 'bg-teal-500 p-0 dark:bg-teal-300' : 'bg-slate-300 p-0 dark:bg-slate-700';
131+
}
132+
function randomInt(min: number, max: number) {
133+
return Math.floor(Math.random() * (max - min) + min);
134+
}
135+
136+
function randomIdeal(week: number, low = false) {
137+
if (week === 15 || week === 35 || week === 36 || week === 54 || Math.random() < 0.08) {
138+
return 0;
139+
}
140+
return randomInt(...(low ? idealLowRanges : idealRanges)[week % 4]!);
141+
}
142+
143+
const idealRanges: Array<[number, number]> = [
144+
[0, 3],
145+
[2, 6],
146+
[2, 8],
147+
[4, 9],
148+
];
149+
150+
const idealLowRanges: Array<[number, number]> = [
151+
[0, 1],
152+
[0, 2],
153+
[0, 3],
154+
[0, 3],
155+
];
156+
157+
function randomBase(week: number) {
158+
return randomInt(...baseRanges[week % baseRanges.length]!);
159+
}
160+
161+
const baseRanges: Array<[number, number]> = [
162+
[0, 1],
163+
[0, 1],
164+
[0, 2],
165+
[0, 3],
166+
[0, 4],
167+
[1, 4],
168+
[1, 3],
169+
[0, 2],
170+
[1, 5],
171+
[1, 8],
172+
[3, 7],
173+
[3, 5],
174+
[3, 8],
175+
[3, 7],
176+
];
177+
178+
function randomSpecial() {
179+
return randomInt(1, 3);
180+
}
181+
182+
const opacity = [
183+
'opacity-10',
184+
'opacity-20',
185+
'opacity-30',
186+
'opacity-40',
187+
'opacity-50',
188+
'opacity-60',
189+
'opacity-70',
190+
'opacity-75',
191+
'opacity-80',
192+
'opacity-90',
193+
'opacity-100',
194+
];
195+
196+
const myContributions = [
197+
0, 0, 1, 3, 0, 3, 0, 0, 1, 4, 4, 2, 1, 0, 0, 1, 5, 11, 10, 0, 0, 0, 1, 0, 5, 3, 27, 0, 3, 3, 3, 1, 3, 6, 4, 5, 4, 14,
198+
4, 17, 13, 1, 0, 23, 23, 27, 36, 28, 13, 9, 14, 19, 36, 19, 28, 10, 10, 34, 8, 29, 37, 7, 12, 3, 50, 43, 30, 15, 29,
199+
17, 20, 28, 14, 10, 40, 26, 0, 18, 35, 45, 47, 8, 8, 0, 13, 30, 22, 9, 19, 34, 0, 5, 40, 26, 20, 17, 7, 0, 1, 32, 5,
200+
12, 12, 24, 0, 0, 31, 4, 9, 6, 0, 0, 0, 16, 1, 17, 6, 5, 0, 0, 18, 6, 7, 8, 23, 13, 0, 10, 1, 2, 2, 1, 0, 0, 8, 1, 3,
201+
2, 5, 0, 0, 32, 9, 10, 13, 3, 0, 0, 0, 9, 7, 1, 5, 0, 1, 20, 20, 9, 5, 7, 0, 0, 5, 6, 1, 0, 0, 0, 0, 0, 0, 6, 7, 6, 0,
202+
3, 3, 6, 3, 5, 7, 0, 18, 3, 3, 6, 7, 8, 1, 0, 3, 15, 19, 7, 9, 1, 4, 10, 4, 4, 3, 3, 3, 0, 6, 1, 2, 6, 10, 0, 0, 6, 1,
203+
6, 3, 8, 0, 0, 0, 0, 0, 0, 0, 0,
204+
];
205+
206+
type Contributor =
207+
| 'full'
208+
| 'underperformer'
209+
| 'weekdays'
210+
| 'ideal'
211+
| 'ideal-low'
212+
| 'base'
213+
| 'build'
214+
| 'specialization'
215+
| 'mine';
216+
217+
export function getContributions({
218+
type,
219+
start = new Date(Date.UTC(2022, 6, 24)),
220+
end = new Date(Date.UTC(2023, 6, 30)),
221+
visibleRange,
222+
}: {
223+
type: Contributor;
224+
start?: Date;
225+
end?: Date;
226+
visibleRange: [number, number];
227+
}) {
228+
let day = new Date(start);
229+
const days: Array<Array<[Date, number, boolean]>> = [[], [], [], [], [], [], []];
230+
let weekNum = 0;
231+
let i = 0;
232+
while (day < end) {
233+
const dayOfWeek = day.getDay();
234+
let contributions = 0;
235+
if (type === 'full') {
236+
contributions = randomInt(3, 10);
237+
} else if (type === 'mine') {
238+
contributions = myContributions[i]!;
239+
} else if (dayOfWeek <= 4) {
240+
switch (type) {
241+
case 'weekdays':
242+
contributions = randomInt(1, 9);
243+
break;
244+
case 'underperformer':
245+
contributions = Math.round(Math.random() * 0.55);
246+
break;
247+
case 'ideal':
248+
contributions = randomIdeal(weekNum);
249+
break;
250+
case 'ideal-low':
251+
contributions = randomIdeal(weekNum, true);
252+
break;
253+
case 'base':
254+
contributions = weekInRange(weekNum, visibleRange) ? randomBase(weekNum) : randomIdeal(weekNum);
255+
break;
256+
case 'build':
257+
contributions = weekInRange(weekNum, visibleRange) ? randomIdeal(weekNum) : randomBase(weekNum);
258+
break;
259+
case 'specialization':
260+
contributions = weekInRange(weekNum, visibleRange) ? randomSpecial() : randomIdeal(weekNum);
261+
break;
262+
}
263+
}
264+
265+
days[dayOfWeek]!.push([day, contributions, weekInRange(weekNum, visibleRange)]);
266+
day = new Date(day.setDate(day.getDate() + 1));
267+
weekNum += dayOfWeek === 6 ? 1 : 0;
268+
i += 1;
269+
}
270+
const sunday = days.pop();
271+
days.unshift(sunday!);
272+
return days;
273+
}

0 commit comments

Comments
 (0)