This commit is contained in:
@@ -54,6 +54,29 @@ extension Item where M == ArticleMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Array where Element == Item<ArticleMetadata> {
|
||||||
|
|
||||||
|
func years() -> Set<String> {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "yyyy"
|
||||||
|
return reduce(into: Set()) { set, item in
|
||||||
|
let date = dateFormatter.string(from: item.getDate())
|
||||||
|
set.insert(date)
|
||||||
|
if let updatedDate = item.getUpdatedDate() {
|
||||||
|
set.insert(dateFormatter.string(from: updatedDate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueTags() -> Set<String> {
|
||||||
|
reduce(into: Set()) { set, item in
|
||||||
|
for tag in item.metadata.tags {
|
||||||
|
set.insert(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: Most of these are taken from https://github.com/loopwerk/loopwerk.io
|
// NOTE: Most of these are taken from https://github.com/loopwerk/loopwerk.io
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import Foundation
|
|||||||
import HTML
|
import HTML
|
||||||
import Saga
|
import Saga
|
||||||
|
|
||||||
|
// TODO: Try a sidebar with navigation to tags & years.
|
||||||
|
|
||||||
/// Displays lists of articles sectioned by a key.
|
/// Displays lists of articles sectioned by a key.
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
@@ -33,6 +35,29 @@ struct ArticleGrid: NodeConvertible {
|
|||||||
self.header = header
|
self.header = header
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var allItems: [Item<ArticleMetadata>] {
|
||||||
|
articles.reduce(into: [Item<ArticleMetadata>]()) { $0 += $1.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sortedYears: [String] {
|
||||||
|
allItems.years().sorted { $0 > $1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sortedTags: [String] {
|
||||||
|
allItems.uniqueTags().sorted { $0 < $1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sidebarLink(
|
||||||
|
_ label: String,
|
||||||
|
href: String,
|
||||||
|
) -> Node {
|
||||||
|
a(class: "text-slate-300 font-semibold [&:hover]:text-slate-200", href: href) {
|
||||||
|
div(class: "flex w-full p-2 [&:hover]:border-b border-orange-400") {
|
||||||
|
span(class: "mx-8") { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func asNode() -> Node {
|
func asNode() -> Node {
|
||||||
baseLayout(
|
baseLayout(
|
||||||
canocicalURL: canocicalURL,
|
canocicalURL: canocicalURL,
|
||||||
@@ -41,7 +66,31 @@ struct ArticleGrid: NodeConvertible {
|
|||||||
rssLink: rssLink,
|
rssLink: rssLink,
|
||||||
extraHeader: extraHeader
|
extraHeader: extraHeader
|
||||||
) {
|
) {
|
||||||
div(class: "mt-8 mb-10 bg-slate-800 border border-slate-200 rounded-lg") {
|
div(class: "grid grid-cols-4 mt-10 mb-10") {
|
||||||
|
// Sidebar
|
||||||
|
div(class: "overflow-auto bg-slate-800 rounded-l-lg border border-slate-200") {
|
||||||
|
// Years
|
||||||
|
section(class: "pt-2") {
|
||||||
|
div(class: "flex ps-2") {
|
||||||
|
span(class: "mt-2 ps-2 font-extrabold text-slate-400") { "YEARS" }
|
||||||
|
}
|
||||||
|
sortedYears.map { year in
|
||||||
|
sidebarLink(year, href: "/articles/\(year)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tags
|
||||||
|
section(class: "pt-2") {
|
||||||
|
div(class: "flex ps-2 pt-2") {
|
||||||
|
span(class: "mt-2 ps-2 font-extrabold text-slate-400") { "TAGS" }
|
||||||
|
}
|
||||||
|
sortedTags.map { tag in
|
||||||
|
sidebarLink(tag, href: "/articles/tag/\(tag)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Articles
|
||||||
|
div(class: "col-span-3 bg-slate-800 border border-slate-200 rounded-r-lg") {
|
||||||
articles.map { key, articles in
|
articles.map { key, articles in
|
||||||
section {
|
section {
|
||||||
header(key)
|
header(key)
|
||||||
@@ -53,4 +102,5 @@ struct ArticleGrid: NodeConvertible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ func baseLayout(
|
|||||||
body(class: "text-white text-lg font-avenir \(section.rawValue)") {
|
body(class: "text-white text-lg font-avenir \(section.rawValue)") {
|
||||||
siteHeader(section)
|
siteHeader(section)
|
||||||
|
|
||||||
div(class: "mb-auto mx-10") {
|
// mx-10
|
||||||
|
div(class: "mb-auto") {
|
||||||
children()
|
children()
|
||||||
}
|
}
|
||||||
if section == .articles {
|
if section == .articles {
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ func renderArticles(context: ItemsRenderingContext<ArticleMetadata>) -> NodeConv
|
|||||||
let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) })
|
let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) })
|
||||||
let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key }
|
let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key }
|
||||||
|
|
||||||
return ArticleGrid(
|
return baseRenderArticles(
|
||||||
articles: sortedByYearDescending,
|
sortedByYearDescending,
|
||||||
canocicalURL: "/articles/",
|
canocicalURL: "/articles/",
|
||||||
title: "Articles",
|
title: "Articles",
|
||||||
rssLink: "",
|
rssLink: "",
|
||||||
extraHeader: "",
|
extraHeader: "",
|
||||||
header: yearHeader(_:)
|
label: yearHeader(_:)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +74,25 @@ private func yearHeader(_ year: String) -> Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func baseRenderArticles(
|
||||||
|
_ articles: [(key: String, value: [Item<ArticleMetadata>])],
|
||||||
|
canocicalURL: String,
|
||||||
|
title pageTitle: String,
|
||||||
|
rssLink: String = "",
|
||||||
|
extraHeader: NodeConvertible = Node.fragment([]),
|
||||||
|
@NodeBuilder label: @escaping (String) -> Node = { _ in Node.fragment([]) }
|
||||||
|
) -> NodeConvertible {
|
||||||
|
ArticleGrid(
|
||||||
|
articles: articles,
|
||||||
|
canocicalURL: canocicalURL,
|
||||||
|
title: pageTitle,
|
||||||
|
rssLink: rssLink,
|
||||||
|
extraHeader: extraHeader
|
||||||
|
) { key in
|
||||||
|
label(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func baseRenderArticles(
|
private func baseRenderArticles(
|
||||||
_ articles: (key: String, value: [Item<ArticleMetadata>]),
|
_ articles: (key: String, value: [Item<ArticleMetadata>]),
|
||||||
canocicalURL: String,
|
canocicalURL: String,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import HTML
|
import HTML
|
||||||
import Saga
|
import Saga
|
||||||
|
|
||||||
|
// TODO: Currently not used, remove in the future.
|
||||||
struct TagGrid: NodeConvertible {
|
struct TagGrid: NodeConvertible {
|
||||||
|
|
||||||
let items: [Item<ArticleMetadata>]
|
let items: [Item<ArticleMetadata>]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user