272 lines
6.6 KiB
Raku
272 lines
6.6 KiB
Raku
use v6.e.PREVIEW;
|
|
|
|
use HTML::Functional;
|
|
use DB::BlogMeta;
|
|
use DB::Post;
|
|
|
|
unit class Config;
|
|
|
|
method generate-head($title, BlogMeta:D $meta, $description?) {
|
|
head [
|
|
meta :charset<utf-8>;
|
|
meta :name<viewport>, :content<width=device-width, initial-scale=1>;
|
|
meta :author :content<Nathan McCarty>;
|
|
do if $title ~~ Str:D {
|
|
title "$title — {$meta.title}";
|
|
} else {
|
|
title $meta.title;
|
|
}
|
|
# Add description, if one exists
|
|
do if $description ~~ Str:D {
|
|
meta :description :content($description)
|
|
} else {
|
|
[]
|
|
}
|
|
# Preconnect to all our resource sources
|
|
link :rel<preconnect> :href<https://static.stranger.systems>;
|
|
link :rel<preconnect> :href<https://fonts.googleapis.com>;
|
|
link :rel<preconnect> :href<https://fonts.gstatic.com> :crossorigin;
|
|
link :rel<preconnect> :href<https://unpkg.com>;
|
|
# Load fonts, Iosevka for code, Open Sans for content, and boxicons for
|
|
# icons
|
|
link :rel<stylesheet>,
|
|
:href<https://static.stranger.systems/fonts/Iosevka/Iosevka.css>;
|
|
link :rel<stylesheet>,
|
|
:href<https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap>;
|
|
link :rel<stylesheet>,
|
|
:href<https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css>;
|
|
# Inline our style sheets
|
|
style %?RESOURCES<colors.css>.slurp;
|
|
style %?RESOURCES<main.css>.slurp;
|
|
style %?RESOURCES<code.css>.slurp;
|
|
];
|
|
}
|
|
|
|
method site-header(BlogMeta:D $meta) {
|
|
header :class<site-header>, [
|
|
div :class<site-logo>, [
|
|
# TODO: Use a real image here
|
|
$meta.title
|
|
];
|
|
div :class<site-tagline>, [
|
|
$meta.tagline
|
|
];
|
|
div :class<header-links>, [
|
|
a :href</index.html>, [
|
|
icon 'home';
|
|
' ';
|
|
span [
|
|
'Home';
|
|
];
|
|
];
|
|
a :href</archive.html>, [
|
|
icon 'archive';
|
|
' ';
|
|
span [
|
|
'Archive';
|
|
];
|
|
];
|
|
a :href</about.html>, [
|
|
icon 'info-circle';
|
|
' ';
|
|
span [
|
|
'About';
|
|
];
|
|
];
|
|
a :href</atom.xml>, [
|
|
icon 'rss';
|
|
' ';
|
|
span [
|
|
'Feed';
|
|
];
|
|
];
|
|
];
|
|
]
|
|
}
|
|
|
|
method post-date(Post:D $post) {
|
|
my $datetime = $post.posted-at;
|
|
my $timestamp = sprintf(
|
|
"%s %02d:%02d%s",
|
|
$datetime.yyyy-mm-dd,
|
|
($datetime.hour % 12) || 12,
|
|
$datetime.minute,
|
|
$datetime.hour < 12 ?? 'am' !! 'pm'
|
|
);
|
|
|
|
div :class<post-time>, :title("Posted At $timestamp"), [
|
|
icon 'time';
|
|
' ';
|
|
$timestamp
|
|
]
|
|
}
|
|
|
|
method post-edit(Post:D $post) {
|
|
return [] unless $post.edited-at.elems;
|
|
my $datetime = $post.edited-at.max;
|
|
my $timestamp = sprintf(
|
|
"%s %02d:%02d%s",
|
|
$datetime.yyyy-mm-dd,
|
|
($datetime.hour % 12) || 12,
|
|
$datetime.minute,
|
|
$datetime.hour < 12 ?? 'am' !! 'pm'
|
|
);
|
|
|
|
div :class<post-edit>, :title("Laste Edited At $timestamp"), [
|
|
icon 'edit';
|
|
' ';
|
|
$timestamp
|
|
]
|
|
}
|
|
|
|
sub mins-to-string($mins) {
|
|
if $mins < 60 {
|
|
$mins.Str ~ "m"
|
|
} else {
|
|
my $h = $mins div 60;
|
|
my $m = $mins mod 60;
|
|
$h.Str ~ "h" ~ $m.Str ~ "m"
|
|
}
|
|
}
|
|
|
|
method post-read-time(Post:D $post) {
|
|
my ($slow, $average, $fast) = $post.readtimes;
|
|
div :class<post-read-time>, :title<Estimated read time at 140/180/220 WPM>, [
|
|
icon 'timer';
|
|
' ';
|
|
mins-to-string $slow;
|
|
' ';
|
|
'/';
|
|
' ';
|
|
mins-to-string $average;
|
|
' ';
|
|
'/';
|
|
' ';
|
|
mins-to-string $fast;
|
|
]
|
|
}
|
|
|
|
method post-info(Post:D $post) {
|
|
div :class<post-info>, [
|
|
self.post-date: $post;
|
|
self.post-edit: $post;
|
|
self.post-read-time: $post;
|
|
# TODO: Add tags once we have support for that
|
|
];
|
|
}
|
|
|
|
method post-header(Post:D $post) {
|
|
header :class<post-header>, [
|
|
div :class<post-title>, [
|
|
h1 $post.title;
|
|
];
|
|
self.post-info: $post;
|
|
]
|
|
}
|
|
|
|
# TODO: Support GFM admonitions
|
|
method generate-post(Post:D $post, BlogMeta:D $meta) {
|
|
my $content = $post.render-html;
|
|
my $head = self.generate-head($post.title, $meta, $post.description);
|
|
my $body =
|
|
body [
|
|
self.site-header: $meta;
|
|
article :class<post>, [
|
|
self.post-header: $post;
|
|
div :class<post-body>, [
|
|
$content;
|
|
]
|
|
]
|
|
];
|
|
# TODO: Setup footer
|
|
# my $footer;
|
|
|
|
my $html = html :lang<en>, [
|
|
$head,
|
|
$body
|
|
];
|
|
|
|
"<!doctype html>$html"
|
|
}
|
|
|
|
method generate-blurb(Int:D $id, $db) {
|
|
my $post = $db.posts{$id};
|
|
my $desc = $post.description;
|
|
my @slugs = $post.all-slugs;
|
|
# Use the primary slug if there is one, the id if there isn't
|
|
my $link = do if @slugs.elems {
|
|
"/posts/by-slug/{@slugs[*-1]}.html"
|
|
} else {
|
|
"/posts/by-id/$id.html"
|
|
}
|
|
div :class<post-blurb>, [
|
|
div :class<post-blurb-title>, [
|
|
a :href($link), span [
|
|
h2 $post.title;
|
|
];
|
|
];
|
|
self.post-info: $post;
|
|
if $desc ~~ Str:D {
|
|
div :class<post-blurb-description>, [
|
|
p $post.description;
|
|
];
|
|
} else {
|
|
[]
|
|
}
|
|
]
|
|
}
|
|
|
|
method generate-index($db) {
|
|
my @most-recent =
|
|
$db.sorted-posts
|
|
.head(10)
|
|
.grep(!*.value.hidden)
|
|
.map(-> $pair {
|
|
self.generate-blurb: $pair.key, $db
|
|
});
|
|
|
|
my $head = self.generate-head(Nil, $db.meta);
|
|
my $body = body [
|
|
self.site-header: $db.meta;
|
|
div :class<post-blurbs>, [
|
|
h1 "Recent Posts"
|
|
], @most-recent;
|
|
];
|
|
|
|
my $html =
|
|
html :lang<en>, [
|
|
$head,
|
|
$body
|
|
];
|
|
|
|
"<!doctype html>$html"
|
|
}
|
|
|
|
method generate-archive($db) {
|
|
my @most-recent =
|
|
$db.sorted-posts
|
|
.grep(!*.value.hidden)
|
|
.map(-> $pair {
|
|
self.generate-blurb: $pair.key, $db
|
|
});
|
|
|
|
my $head = self.generate-head(Nil, $db.meta);
|
|
my $body = body [
|
|
self.site-header: $db.meta;
|
|
div :class<post-blurbs>, [
|
|
h1 "All Posts"
|
|
], @most-recent;
|
|
];
|
|
|
|
my $html =
|
|
html :lang<en>, [
|
|
$head,
|
|
$body
|
|
];
|
|
|
|
"<!doctype html>$html"
|
|
}
|
|
|
|
sub icon($icon) {
|
|
i(:class("bx bx-$icon"))
|
|
}
|