Generate series pages

This commit is contained in:
Nathan McCarty 2025-02-09 05:28:59 -05:00
parent 7cf4827d0c
commit baf8d6556b
8 changed files with 168 additions and 41 deletions

View file

@ -10,14 +10,6 @@ use DB::Post;
unit class Config; unit class Config;
sub show-html($html) {
my $out = "<!doctype html>$html";
# Work around HTML::Functional automatically putting newlines between tags
$out ~~ s:g/'</i>' \v+ '<span>'/<\/i><span>/;
$out ~~ s:g/\v+ '</a>'/<\/a>/;
$out
}
# TODO: Support GFM admonitions # TODO: Support GFM admonitions
method generate-post(Int:D $id, Post:D $post, $db) { method generate-post(Int:D $id, Post:D $post, $db) {
my $meta = $db.meta; my $meta = $db.meta;
@ -45,34 +37,13 @@ method generate-post(Int:D $id, Post:D $post, $db) {
show-html $html show-html $html
} }
method generate-blurb(Int:D $id, $db) {
my $post = $db.posts{$id};
my $desc = $post.description;
my $link = post-link $id, $post;
div :class<post-blurb>, [
div :class<post-blurb-title>, [
a :href($link), span [
h2 $post.title;
];
];
post-info $id, $post, $db;
if $desc ~~ Str:D {
div :class<post-blurb-description>, [
p $post.description;
];
} else {
[]
}
]
}
method generate-index($db) { method generate-index($db) {
my @most-recent = my @most-recent =
$db.sorted-posts $db.sorted-posts
.head(10) .head(10)
.grep(!*.value.hidden) .grep(!*.value.hidden)
.map(-> $pair { .map(-> $pair {
self.generate-blurb: $pair.key, $db generate-blurb $pair.key, $db
}); });
my $head = generate-head($db.meta); my $head = generate-head($db.meta);
@ -97,7 +68,7 @@ method generate-archive($db) {
$db.sorted-posts $db.sorted-posts
.grep(!*.value.hidden) .grep(!*.value.hidden)
.map(-> $pair { .map(-> $pair {
self.generate-blurb: $pair.key, $db generate-blurb $pair.key, $db
}); });
my $head = generate-head($db.meta); my $head = generate-head($db.meta);

View file

@ -14,6 +14,7 @@ use DB::BlogMeta;
use DB::MarkdownPost; use DB::MarkdownPost;
use DB::IdrisPost; use DB::IdrisPost;
use DB::PlaceholderPost; use DB::PlaceholderPost;
use Render::Series;
use Atom; use Atom;
use Config; use Config;
@ -140,6 +141,13 @@ class PostDB {
$config.generate-tag-page(self, $tag); $config.generate-tag-page(self, $tag);
} }
# TODO: Generate the series pages # TODO: Generate the series pages
my $series-dir = $out-dir.add('series/');
mkdir $series-dir unless $series-dir.e;
for %!series.kv -> $key, $value {
$series-dir.add("$key.html").spurt:
series-page($key, self);
}
# TODO: Generate the main series page
# Render the rss/atom feed # Render the rss/atom feed
my $atom-path = $out-dir.add('atom.xml'); my $atom-path = $out-dir.add('atom.xml');
my $atom = posts-to-atom self; my $atom = posts-to-atom self;

View file

@ -22,3 +22,14 @@ method contains-post(Int:D $post-id --> Bool:D) {
False False
} }
} }
#| Returns the date of the lastest post
method latest-post($db) {
my @posts = @!post-ids.map(-> $i {$db.posts{$i}});
if @posts {
my $most-recent-post = @posts.max(*.posted-at);
$most-recent-post.posted-at
} else {
Nil
}
}

View file

@ -131,3 +131,24 @@ sub post-header(Int:D $id, Post:D $post, $db) is export {
post-info $id, $post, $db; post-info $id, $post, $db;
] ]
} }
sub generate-blurb(Int:D $id, $db) is export {
my $post = $db.posts{$id};
my $desc = $post.description;
my $link = post-link $id, $post;
div :class<post-blurb>, [
div :class<post-blurb-title>, [
a :href($link), span [
h2 $post.title;
];
];
post-info $id, $post, $db;
if $desc ~~ Str:D {
div :class<post-blurb-description>, [
p $post.description;
];
} else {
[]
}
]
}

105
lib/Render/Series.rakumod Normal file
View file

@ -0,0 +1,105 @@
use v6.e.PREVIEW;
unit module Render::Series;
use Render::Util;
use Render::Head;
use Render::Post;
use DB::Post;
use DB::Series;
use HTML::Functional;
sub series-date(Series:D $series, $db) is export {
my $datetime = $series.latest-post: $db;
if $datetime {
my $timestamp = sprintf(
"%s %02d:%02d%s",
$datetime.yyyy-mm-dd,
($datetime.hour % 12) || 12,
$datetime.minute,
$datetime.hour < 12 ?? 'am' !! 'pm'
);
div :class<series-time>, :title("Latest post made at $timestamp"), [
icon 'calendar';
'&nbsp;';
$datetime.Date.Str
]
} else {
[]
}
}
sub series-read-time(Series:D $series, $db) is export {
my @readtimes = $series.post-ids.map(-> $i {$db.posts{$i}.readtimes});
my ($slow, $average, $fast) = 0, 0, 0;
for @readtimes -> ($s, $a, $f) {
$slow += $s;
$average += $a;
$fast += $f;
}
div :class<series-read-time>, :title<Estimated read time at 140/180/220 WPM>, [
icon 'timer';
'&nbsp;';
mins-to-string $slow;
'&nbsp;';
'/';
'&nbsp;';
mins-to-string $average;
'&nbsp;';
'/';
'&nbsp;';
mins-to-string $fast;
]
}
sub series-count(Series:D $series, $db) is export {
my $count = $series.post-ids.elems;
div :class<series-article-count>, :title("Series has $count articles"), [
icon 'add-to-queue';
'&nbsp;';
"$count articles";
]
}
sub series-info(Series:D $series, $db) is export {
div :class<series-info>, [
series-date $series, $db;
series-read-time $series, $db;
series-count $series, $db;
]
}
sub series-header(Series:D $series, $db) is export {
header :class<series-header>, [
div :class<series-title>, [
h1 $series.title;
];
div :class<series-desc>, [
p $series.desc;
];
series-info $series, $db;
]
}
sub series-page(Int:D $series-id, $db) is export {
my $meta = $db.meta;
my $series = $db.series{$series-id};
my $head = generate-head($meta, $series.title, $series.desc);
my $body =
body [
site-header $meta;
article :class<series>, [
series-header $series, $db;
div :class<series-blurbs>,
$series.post-ids.map(*.&generate-blurb($db));
]
];
my $html = html :lang<en>, [
$head;
$body
];
show-html $html;
}

View file

@ -5,6 +5,15 @@ use DB::Post;
use HTML::Functional; use HTML::Functional;
sub show-html($html) is export {
my $out = "<!doctype html>$html";
# Work around HTML::Functional automatically putting newlines between tags
$out ~~ s:g/'</i>' \v+ '<span>'/<\/i><span>/;
$out ~~ s:g/\v+ '</a>'/<\/a>/;
$out ~~ s:g/',' \v+ '<span'/,<span/;
$out
}
sub opt($test, $item) is export { sub opt($test, $item) is export {
if $test { if $test {
$item $item

View file

@ -53,7 +53,8 @@ a:visited {
.site-tagline { .site-tagline {
color: var(--dim-0); color: var(--dim-0);
} }
.post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post { .post-body, .post-header, .post-blurbs, .tags, .tags .tag-blurb-post,
.series-header, .series-blurbs {
background-color: var(--bg-0); background-color: var(--bg-0);
} }
.post-blurb, .tags .tag-blurb { .post-blurb, .tags .tag-blurb {
@ -65,13 +66,13 @@ a:visited {
:not(.tags) .tag-blurb-post { :not(.tags) .tag-blurb-post {
background-color: var(--bg-1); background-color: var(--bg-1);
} }
.post-title, .post-blurbs h1 { .post-title, .post-blurbs h1, .series-header h1 {
color: var(--green); color: var(--green);
} }
.post-body h2, .post-body h3, .post-body h4 { .post-body h2, .post-body h3, .post-body h4 {
color: var(--fg-1); color: var(--fg-1);
} }
.post-info > * { .post-info > *, .series-info > * {
background-color: var(--bg-2); background-color: var(--bg-2);
} }
blockquote { blockquote {

View file

@ -27,13 +27,13 @@
} }
/* Main Body and Post Flexboxs */ /* Main Body and Post Flexboxs */
body, .post { body, .post, .series {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: var(--box-gap); gap: var(--box-gap);
} }
.post { .post, .series {
width: 100%; width: 100%;
} }
@ -75,7 +75,7 @@ body, .post {
/* Style the post header, body, and blurbs */ /* Style the post header, body, and blurbs */
/* TODO: Style footnotes and get footnote hover working */ /* TODO: Style footnotes and get footnote hover working */
.post-header, .post-body { .post-header, .post-body, .series-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -90,11 +90,11 @@ body, .post {
margin: auto var(--box-margin-horz); margin: auto var(--box-margin-horz);
align-self: stretch; align-self: stretch;
} }
.post-title h1 { .post-title h1, .series-title h1 {
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
} }
.post-info { .post-info, .series-info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -108,7 +108,7 @@ body, .post {
.post-body h2, .post-body h3, .post-body h4 { .post-body h2, .post-body h3, .post-body h4 {
text-align: center; text-align: center;
} }
.post-blurbs { .post-blurbs, .series-blurbs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -116,6 +116,7 @@ body, .post {
max-width: var(--content-width); max-width: var(--content-width);
padding: var(--box-padding-vert) var(--box-padding-horz); padding: var(--box-padding-vert) var(--box-padding-horz);
border-radius: var(--box-radius); border-radius: var(--box-radius);
box-sizing: border-box;
} }
.post-blurb { .post-blurb {
width: 100%; width: 100%;
@ -128,7 +129,7 @@ body, .post {
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
.post-info > * { .post-info > *, .series-info > * {
padding: 0.25em; padding: 0.25em;
border-radius: 0.25em; border-radius: 0.25em;
} }