Format links page
Group links by date and style them like cards.
This commit is contained in:
parent
8a1313eb6e
commit
9f15da0835
2 changed files with 127 additions and 14 deletions
|
@ -12,6 +12,7 @@ interface LinkData {
|
|||
description?: string,
|
||||
read?: string,
|
||||
author?: string,
|
||||
quote?: string,
|
||||
}
|
||||
interface Data {
|
||||
links: LinkData[];
|
||||
|
@ -23,13 +24,81 @@ interface Data {
|
|||
function Link(props: { link: LinkData }) {
|
||||
const link = props.link;
|
||||
return <>
|
||||
<a href={link.url}>{link.title}</a>
|
||||
{" "}
|
||||
{link.tags.join(", ")}
|
||||
<hgroup>
|
||||
<h4>
|
||||
<a href={link.url}>{link.title}</a>
|
||||
{link.author ? <span class="no-break">– {link.author}</span> : null}
|
||||
</h4>
|
||||
<p>
|
||||
{link.tags.join(", ")}
|
||||
{link.read ? <>
|
||||
, read: <time>{link.read}</time>
|
||||
</> : null}
|
||||
</p>
|
||||
</hgroup>
|
||||
{link.quote ? <blockquote>
|
||||
{link.quote}
|
||||
</blockquote> : null}
|
||||
{link.description ? <p>
|
||||
{link.description}
|
||||
</p> : null}
|
||||
{link.altUrls ? <p>
|
||||
Also available at {link.altUrls.map(url => <a href={url}>{url}</a>)}.
|
||||
</p> : null}
|
||||
{link.related ? <p>
|
||||
Related <a href={link.related}>{link.related}</a>.
|
||||
</p> : null}
|
||||
{link.via ? <p>
|
||||
Via <a href={link.via}>{link.via}</a>.
|
||||
</p> : null}
|
||||
</>
|
||||
}
|
||||
const data: Data = eval(`(${readFileSync("src/content/links.jsonc", "utf8")})`);
|
||||
data.links.pop(); // Remove template at the end
|
||||
|
||||
function compare(a: string, b: string) {
|
||||
return Number(a > b) - Number(b > a);
|
||||
}
|
||||
|
||||
function* groupBy<T, K>(items: Iterable<T>, keyFn: (item: T) => K) {
|
||||
let oldKey: K = Symbol() as any;
|
||||
let group: T[] | undefined = undefined;
|
||||
for (const item of items) {
|
||||
let newKey = keyFn(item);
|
||||
if (!Object.is(newKey, oldKey)) {
|
||||
if (group) {
|
||||
yield [oldKey, group] as [K, T[]];
|
||||
}
|
||||
group = [];
|
||||
oldKey = newKey;
|
||||
}
|
||||
group!.push(item);
|
||||
}
|
||||
if (group) {
|
||||
yield [oldKey, group] as [K, T[]];
|
||||
}
|
||||
}
|
||||
|
||||
const byDate = data.links.filter(link => link.read).sort((a, b) => -compare(a.read!, b.read!));
|
||||
const byMonth = groupBy(byDate, link => link.read!.slice(0, 7));
|
||||
const byYearMonth = [...groupBy(byMonth, month => month[0].slice(0, 4))];
|
||||
const other = data.links.filter(link => !link.read);
|
||||
const monthNames = [
|
||||
"", // padding to make mapping start at 1
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
|
||||
const title = "Links!";
|
||||
export const links: Page = {
|
||||
title,
|
||||
|
@ -37,8 +106,27 @@ export const links: Page = {
|
|||
content: <BasePage title={title}>
|
||||
<main>
|
||||
<h1>{title}</h1>
|
||||
<ul>
|
||||
{ data.links.map(link => <li><Link link={link} /></li>)}
|
||||
<p>
|
||||
Here be interesting things I've read, watched, listened to or otherwise found useful as resources over the years. These are ordered by when I read them as that seems most practical to me.
|
||||
</p>
|
||||
{byYearMonth.map(([year, months]) => <section>
|
||||
<h2>{year}</h2>
|
||||
{months.map(([yearMonth, links]) => <section>
|
||||
<h3>{monthNames[Number.parseInt(yearMonth.slice(5))]}</h3>
|
||||
<ul role="list">
|
||||
{links.map(link => <li class="link">
|
||||
<Link link={link} />
|
||||
</li>)}
|
||||
</ul>
|
||||
</section>)}
|
||||
</section>)}
|
||||
<h2>
|
||||
Resources and other things
|
||||
</h2>
|
||||
<ul role="list">
|
||||
{other.map(link => <li class="link">
|
||||
<Link link={link} />
|
||||
</li>)}
|
||||
</ul>
|
||||
</main>
|
||||
</BasePage>
|
||||
|
|
|
@ -14,6 +14,10 @@ html {
|
|||
text-size-adjust: none;
|
||||
}
|
||||
|
||||
:is(ul, ol)[role="list"] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, button, input, label {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
@ -44,12 +48,12 @@ html {
|
|||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
hgroup h1 {
|
||||
margin-bottom: 0.1em;
|
||||
hgroup :is(h1, h2, h3, h4) {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0.1em;
|
||||
}
|
||||
hgroup p {
|
||||
font-style: italic;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
|
@ -57,11 +61,21 @@ h1, h2, h3, h4 {
|
|||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
||||
:is(p, ol, ul) + :is(p, ol, ul) {
|
||||
padding-block-start: 1em;
|
||||
:is(hgroup, p, blockquote, ol, ul, li) + :is(hgroup, blockquote, p, ol, ul, li) {
|
||||
margin-block-start: var(--block-space, 1em);
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
blockquote {
|
||||
padding-inline: 1.25em;
|
||||
}
|
||||
blockquote::before {
|
||||
content: "“";
|
||||
}
|
||||
blockquote::after {
|
||||
content: "”";
|
||||
}
|
||||
|
||||
:is(ol, ul):not([role]) {
|
||||
padding-inline-start: 1.25em;
|
||||
}
|
||||
|
||||
|
@ -102,13 +116,24 @@ body {
|
|||
gap: 1em;
|
||||
margin-block: 1em;
|
||||
}
|
||||
.author h1 {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
.author p {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
/* links */
|
||||
.no-break {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
.link {
|
||||
background-color: color-mix(in oklab, Canvas 90%, white);
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.link>* {
|
||||
--block-space: 0.5em;
|
||||
}
|
||||
|
||||
/* sandbox */
|
||||
.sandbox-inset-3d {
|
||||
contain: paint;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue