Switch over to WebAssembly, Rust and Yew #35
144
Cargo.lock
generated
144
Cargo.lock
generated
@ -32,6 +32,17 @@ version = "0.13.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.73"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.29",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -145,6 +156,12 @@ dependencies = [
|
|||||||
"termcolor",
|
"termcolor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -433,12 +450,29 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gray_matter"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cf2fb99fac0b821a4e61c61abff076324bb0e5c3b4a83815bbc3518a38971ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"yaml-rust",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -477,7 +511,26 @@ version = "0.3.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f"
|
checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e"
|
||||||
|
dependencies = [
|
||||||
|
"include_dir_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "include_dir_macros"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -487,7 +540,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown",
|
"hashbrown 0.12.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown 0.14.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -522,6 +585,12 @@ version = "0.2.147"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@ -880,6 +949,19 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.0.0",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -893,13 +975,22 @@ dependencies = [
|
|||||||
name = "site"
|
name = "site"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"gray_matter",
|
||||||
|
"include_dir",
|
||||||
"log",
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-logger",
|
"wasm-logger",
|
||||||
|
"words-count",
|
||||||
"yew",
|
"yew",
|
||||||
|
"yew-hooks",
|
||||||
"yew-router",
|
"yew-router",
|
||||||
"yew_icons",
|
"yew_icons",
|
||||||
]
|
]
|
||||||
@ -988,6 +1079,7 @@ checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"num_threads",
|
"num_threads",
|
||||||
"serde",
|
"serde",
|
||||||
@ -1083,12 +1175,24 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-blocks"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c84398c527c802fbf222e5145f220382d60f1878e0e6cb4d22a3080949a8ddcd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8-width"
|
name = "utf8-width"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -1291,6 +1395,24 @@ version = "0.48.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "words-count"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c569acc49de9affbc9ace57633381bad6d3e9c648605206d082deb7aa62e8cf5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-blocks",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||||
|
dependencies = [
|
||||||
|
"linked-hash-map",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew"
|
name = "yew"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@ -1304,7 +1426,7 @@ dependencies = [
|
|||||||
"gloo",
|
"gloo",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
"implicit-clone",
|
"implicit-clone",
|
||||||
"indexmap",
|
"indexmap 1.9.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"prokio",
|
"prokio",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@ -1319,6 +1441,22 @@ dependencies = [
|
|||||||
"yew-macro",
|
"yew-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yew-hooks"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "268e2367720311f19582235f5c021702d6be8ded13b7ee8dcacc71019d055d15"
|
||||||
|
dependencies = [
|
||||||
|
"gloo",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"yew",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yew-macro"
|
name = "yew-macro"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
|
22
Cargo.toml
22
Cargo.toml
@ -23,9 +23,18 @@ required-features = [
|
|||||||
name = "site"
|
name = "site"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = { version = "0.1" }
|
||||||
|
gray_matter = { version = "0.2", default-features = false, features = ["yaml"] }
|
||||||
|
include_dir = { version = "0.7" }
|
||||||
yew = { version = "0.20" }
|
yew = { version = "0.20" }
|
||||||
|
yew-hooks = { version = "0.2" }
|
||||||
yew-router = { version = "0.17" }
|
yew-router = { version = "0.17" }
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0" }
|
||||||
|
serde_yaml = { version = "0.9" }
|
||||||
|
thiserror = { version = "1.0" }
|
||||||
|
words-count = { version = "0.1" }
|
||||||
|
|
||||||
[dependencies.time]
|
[dependencies.time]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
@ -34,20 +43,19 @@ features = [
|
|||||||
"local-offset",
|
"local-offset",
|
||||||
"macros",
|
"macros",
|
||||||
"parsing",
|
"parsing",
|
||||||
"serde"
|
"serde",
|
||||||
|
"wasm-bindgen"
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies.yew_icons]
|
[dependencies.yew_icons]
|
||||||
version = "0.7"
|
version = "0.7"
|
||||||
features = [
|
features = [
|
||||||
|
"BootstrapDot",
|
||||||
|
"BootstrapGithub",
|
||||||
|
"BootstrapMastodon",
|
||||||
|
"LucideMenu",
|
||||||
"LucideRss"
|
"LucideRss"
|
||||||
]
|
]
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")']
|
|
||||||
required-features = [
|
|
||||||
"time/wasm-bindgen"
|
|
||||||
]
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
wasm-bindgen-futures = { version = "0.4" }
|
wasm-bindgen-futures = { version = "0.4" }
|
||||||
wasm-logger = { version = "0.2" }
|
wasm-logger = { version = "0.2" }
|
||||||
|
@ -2,141 +2,118 @@
|
|||||||
# tags.yaml
|
# tags.yaml
|
||||||
#
|
#
|
||||||
|
|
||||||
getting-started:
|
- slug: getting-started
|
||||||
slug: getting-started
|
|
||||||
name: Getting Started
|
name: Getting Started
|
||||||
visibility: public
|
visibility: public
|
||||||
|
|
||||||
haskell:
|
- slug: haskell
|
||||||
slug: haskell
|
|
||||||
name: Haskell
|
name: Haskell
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the Haskell programming language.
|
description: Posts that feature the Haskell programming language.
|
||||||
|
|
||||||
reviews:
|
- slug: reviews
|
||||||
slug: reviews
|
|
||||||
name: Reviews
|
name: Reviews
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Reviews of books or other articles
|
description: Reviews of books or other articles
|
||||||
|
|
||||||
electronics:
|
- slug: electronics
|
||||||
slug: electronics
|
|
||||||
name: Electronics
|
name: Electronics
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature electronic circuits.
|
description: Posts that feature electronic circuits.
|
||||||
|
|
||||||
javascript:
|
- slug: javascript
|
||||||
slug: javascript
|
|
||||||
name: JavaScript
|
name: JavaScript
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the JavaScript scripting language.
|
description: Posts that feature the JavaScript scripting language.
|
||||||
|
|
||||||
python:
|
- slug: python
|
||||||
slug: python
|
|
||||||
name: Python
|
name: Python
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the Python scripting language
|
description: Posts that feature the Python scripting language
|
||||||
|
|
||||||
ghost-tag:
|
- slug: ghost-tag
|
||||||
slug: ghost-tag
|
|
||||||
name: Ghost
|
name: Ghost
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts related to my use of the Ghost CMS
|
description: Posts related to my use of the Ghost CMS
|
||||||
|
|
||||||
gtk:
|
- slug: gtk
|
||||||
slug: gtk
|
|
||||||
name: GTK
|
name: GTK
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the GTK graphical user interface toolkit.
|
description: Posts that feature the GTK graphical user interface toolkit.
|
||||||
|
|
||||||
aws:
|
- slug: aws
|
||||||
slug: aws
|
|
||||||
name: AWS
|
name: AWS
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that relate to Amazon Web Services
|
description: Posts that relate to Amazon Web Services
|
||||||
|
|
||||||
linux:
|
- slug: linux
|
||||||
slug: linux
|
|
||||||
name: Linux
|
name: Linux
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the Linux operating system.
|
description: Posts that feature the Linux operating system.
|
||||||
|
|
||||||
cairo:
|
- slug: cairo
|
||||||
slug: cairo
|
|
||||||
name: Cairo
|
name: Cairo
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the Cairo vector-based graphics library.
|
description: Posts that feature the Cairo vector-based graphics library.
|
||||||
|
|
||||||
cpp:
|
- slug: cpp
|
||||||
slug: cpp
|
|
||||||
name: C++
|
name: C++
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the C++ programming language.
|
description: Posts that feature the C++ programming language.
|
||||||
|
|
||||||
yesod:
|
- slug: yesod
|
||||||
slug: yesod
|
|
||||||
name: Yesod
|
name: Yesod
|
||||||
visibility: public
|
visibility: public
|
||||||
description:
|
description:
|
||||||
Posts which feature the Yesod web application development framework
|
Posts which feature the Yesod web application development framework
|
||||||
for the Haskell programming language.
|
for the Haskell programming language.
|
||||||
|
|
||||||
ethernet:
|
- slug: ethernet
|
||||||
slug: ethernet
|
|
||||||
name: Ethernet
|
name: Ethernet
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that relate to Ethernet networking
|
description: Posts that relate to Ethernet networking
|
||||||
|
|
||||||
react:
|
- slug: react
|
||||||
slug: react
|
|
||||||
name: React
|
name: React
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that make use of the React JavaScript library.
|
description: Posts that make use of the React JavaScript library.
|
||||||
|
|
||||||
gc:
|
- slug: gc
|
||||||
slug: gc
|
|
||||||
name: GC
|
name: GC
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that relate to garbage collection and memory management.
|
description: Posts that relate to garbage collection and memory management.
|
||||||
|
|
||||||
sqlite:
|
- slug: sqlite
|
||||||
slug: sqlite
|
|
||||||
name: SQLite
|
name: SQLite
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the SQLite embedded relational database.
|
description: Posts that feature the SQLite embedded relational database.
|
||||||
|
|
||||||
chakracore:
|
- slug: chakracore
|
||||||
slug: chakracore
|
|
||||||
name: ChakraCore
|
name: ChakraCore
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the ChakraCore JavaScript engine.
|
description: Posts that feature the ChakraCore JavaScript engine.
|
||||||
|
|
||||||
pci:
|
- slug: pci
|
||||||
slug: pci
|
|
||||||
name: PCI
|
name: PCI
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Articles related to programming PCI express devices.
|
description: Articles related to programming PCI express devices.
|
||||||
|
|
||||||
usb:
|
- slug: usb
|
||||||
slug: usb
|
|
||||||
name: USB
|
name: USB
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that relate to working with the Universal Serial Bus
|
description: Posts that relate to working with the Universal Serial Bus
|
||||||
|
|
||||||
intel-i350:
|
- slug: intel-i350
|
||||||
slug: intel-i350
|
|
||||||
name: Intel i350
|
name: Intel i350
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Blog posts about programming the Intel i350 NIC and related models.
|
description: Blog posts about programming the Intel i350 NIC and related models.
|
||||||
|
|
||||||
rust:
|
- slug: rust
|
||||||
slug: rust
|
|
||||||
name: Rust
|
name: Rust
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Blog posts related to the Rust programming language
|
description: Blog posts related to the Rust programming language
|
||||||
|
|
||||||
mastodon:
|
- slug: mastodon
|
||||||
slug: mastodon
|
|
||||||
name: Mastodon
|
name: Mastodon
|
||||||
visibility: public
|
visibility: public
|
||||||
description: Posts that feature the Mastodon social network.
|
description: Posts that feature the Mastodon social network.
|
||||||
|
@ -4,7 +4,15 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Blake Rain</title>
|
<title>Blake Rain</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Noto+Serif:wght@400;700&family=Roboto:wght@100;300;400;500;700&family=Source+Code+Pro:wght@400;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
<link data-trunk rel="rust" data-bin="site" />
|
<link data-trunk rel="rust" data-bin="site" />
|
||||||
<link data-trunk rel="css" href="target/main.css" />
|
<link data-trunk rel="css" href="target/main.css" />
|
||||||
|
<link data-trunk rel="copy-dir" href="public/media" />
|
||||||
|
<link data-trunk rel="copy-dir" href="public/content" />
|
||||||
</head>
|
</head>
|
||||||
</html>
|
</html>
|
||||||
|
18
src/app.rs
18
src/app.rs
@ -10,14 +10,24 @@ use yew_router::{
|
|||||||
#[cfg(not(feature = "static"))]
|
#[cfg(not(feature = "static"))]
|
||||||
use yew_router::BrowserRouter;
|
use yew_router::BrowserRouter;
|
||||||
|
|
||||||
use crate::pages::Route;
|
use crate::{
|
||||||
|
components::layout::Layout,
|
||||||
|
model::{source::ModelProvider, tags::TagsProvider},
|
||||||
|
pages::Route,
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component(AppContent)]
|
#[function_component(AppContent)]
|
||||||
fn app_content() -> Html {
|
fn app_content() -> Html {
|
||||||
html! {
|
html! {
|
||||||
<main>
|
<ModelProvider>
|
||||||
<Switch<Route> render={Route::switch} />
|
<TagsProvider>
|
||||||
</main>
|
<Layout>
|
||||||
|
<main>
|
||||||
|
<Switch<Route> render={Route::switch} />
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
</TagsProvider>
|
||||||
|
</ModelProvider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
src/components.rs
Normal file
2
src/components.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod blog;
|
||||||
|
pub mod layout;
|
2
src/components/blog.rs
Normal file
2
src/components/blog.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod post_card;
|
||||||
|
pub mod post_card_list;
|
112
src/components/blog/post_card.rs
Normal file
112
src/components/blog/post_card.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
use time::{format_description::FormatItem, macros::format_description};
|
||||||
|
use yew::{function_component, html, use_context, Html, Properties};
|
||||||
|
use yew_icons::{Icon, IconId};
|
||||||
|
use yew_router::prelude::Link;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::layout::intersperse::Intersperse,
|
||||||
|
model::{tags::TagsContext, PostInfo},
|
||||||
|
pages::Route,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DATE_FORMAT: &[FormatItem] =
|
||||||
|
format_description!("[day padding:none] [month repr:short] [year]");
|
||||||
|
|
||||||
|
fn post_card_image(slug: &str, image: &Option<String>) -> Html {
|
||||||
|
html! {
|
||||||
|
<Link<Route> classes="unstyled" to={Route::BlogPost { slug: slug.to_string() }}>
|
||||||
|
<div class="relative w-full h-[240px]">
|
||||||
|
if let Some(cover_image) = image {
|
||||||
|
<img class="rounded-xl object-cover absolute w-full h-full"
|
||||||
|
src={cover_image.clone()} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link<Route>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_card_details(info: &PostInfo, tags: &TagsContext) -> Html {
|
||||||
|
let mut facts =
|
||||||
|
Intersperse::new(html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> });
|
||||||
|
|
||||||
|
if let Some(published) = info.doc_info.published {
|
||||||
|
facts.push(html! { <div>{published.format(DATE_FORMAT).expect("valid format")}</div> });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reading_time) = info.reading_time {
|
||||||
|
facts.push(html! { <div>{format!("{reading_time} min read")}</div> })
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = Intersperse::from_iter(
|
||||||
|
html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> },
|
||||||
|
info.tags.iter().map(|tag| {
|
||||||
|
if let Some(tag) = tags.get_tag(tag) {
|
||||||
|
html! {
|
||||||
|
<Link<Route>
|
||||||
|
classes="text-sky-500 hover:text-sky-600"
|
||||||
|
to={Route::Tag { slug: tag.slug }}>{tag.name}</Link<Route>>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<span class="text-gray-500">{tag}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="flex flex-col uppercase text-sm">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
{facts.finish()}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
{tags.finish()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_card_description(info: &PostInfo, tags: &TagsContext) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="grow flex flex-col gap-4 justify-between">
|
||||||
|
<Link<Route> classes="unstyled" to={Route::BlogPost { slug: info.doc_info.slug.clone() }}>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<h1 class="text-2xl font-bold">{&info.doc_info.title}</h1>
|
||||||
|
if let Some(excerpt) = &info.doc_info.excerpt {
|
||||||
|
<p class="text-gray-500 font-text text-xl">{excerpt}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link<Route>>
|
||||||
|
{post_card_details(info, tags)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct PostCardProps {
|
||||||
|
pub first: bool,
|
||||||
|
pub post: PostInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(PostCard)]
|
||||||
|
pub fn post_card(props: &PostCardProps) -> Html {
|
||||||
|
let tags = use_context::<TagsContext>().expect("TagsContext to be provided");
|
||||||
|
|
||||||
|
if props.first {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{post_card_image(&props.post.doc_info.slug, &props.post.cover_image)}
|
||||||
|
<div class="col-span-2">
|
||||||
|
{post_card_description(&props.post, &tags)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
{post_card_image(&props.post.doc_info.slug, &props.post.cover_image)}
|
||||||
|
{post_card_description(&props.post, &tags)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/components/blog/post_card_list.rs
Normal file
21
src/components/blog/post_card_list.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
|
||||||
|
use crate::{components::blog::post_card::PostCard, model::PostInfo};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct PostCardListProps {
|
||||||
|
pub posts: Vec<PostInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(PostCardList)]
|
||||||
|
pub fn post_card_list(props: &PostCardListProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="grid grid-cols-3 gap-x-10 gap-y-20 my-10">
|
||||||
|
{for props.posts.iter().enumerate().map(|(index, post)| {
|
||||||
|
html! {
|
||||||
|
<PostCard post={post.clone()} first={index == 0} />
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
24
src/components/layout.rs
Normal file
24
src/components/layout.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use yew::{function_component, html, Children, Html, Properties};
|
||||||
|
|
||||||
|
mod footer;
|
||||||
|
pub mod intersperse;
|
||||||
|
mod navigation;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct LayoutProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(Layout)]
|
||||||
|
pub fn layout(props: &LayoutProps) -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<navigation::Navigation />
|
||||||
|
<div class="container mx-auto">
|
||||||
|
{props.children.clone()}
|
||||||
|
</div>
|
||||||
|
<footer::Footer />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
46
src/components/layout/footer.rs
Normal file
46
src/components/layout/footer.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use time::OffsetDateTime;
|
||||||
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
use yew_router::prelude::Link;
|
||||||
|
|
||||||
|
use crate::pages::Route;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct FooterProps {}
|
||||||
|
|
||||||
|
#[function_component(Footer)]
|
||||||
|
pub fn footer(_: &FooterProps) -> Html {
|
||||||
|
let year = OffsetDateTime::now_utc().year();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="bg-primary text-neutral-400 text-sm mt-4">
|
||||||
|
<div class="container mx-auto flex flex-col gap-4 md:gap-0 md:flex-row md:justify-between px-4 sm:px-0 py-6">
|
||||||
|
<div>{format!("Blake Rain © {year}")}</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 md:gap-3">
|
||||||
|
<Link<Route> classes="hover:text-neutral-50" to={Route::Blog}>
|
||||||
|
{"Latest Posts"}
|
||||||
|
</Link<Route>>
|
||||||
|
<Link<Route> classes="hover:text-neutral-50" to={Route::Tags}>
|
||||||
|
{"Tags"}
|
||||||
|
</Link<Route>>
|
||||||
|
<Link<Route> classes="hover:text-neutral-50" to={Route::Disclaimer}>
|
||||||
|
{"Disclaimer"}
|
||||||
|
</Link<Route>>
|
||||||
|
<a href="https://github.com/BlakeRain"
|
||||||
|
class="hover:text-neutral-50"
|
||||||
|
title="GitHub"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
{"GitHub"}
|
||||||
|
</a>
|
||||||
|
<a href="https://mastodonapp.uk/@BlakeRain"
|
||||||
|
class="hover:text-neutral-50"
|
||||||
|
title="@BlakeRain@mastodonapp.uk"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
{"Mastodon"}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
40
src/components/layout/intersperse.rs
Normal file
40
src/components/layout/intersperse.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use yew::Html;
|
||||||
|
|
||||||
|
pub struct Intersperse {
|
||||||
|
inner: Vec<Html>,
|
||||||
|
separator: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Intersperse {
|
||||||
|
pub fn new(separator: Html) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Vec::new(),
|
||||||
|
separator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_iter<I>(separator: Html, elements: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Html>,
|
||||||
|
{
|
||||||
|
let mut v = Self::new(separator);
|
||||||
|
|
||||||
|
for element in elements {
|
||||||
|
v.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, element: Html) {
|
||||||
|
if !self.inner.is_empty() {
|
||||||
|
self.inner.push(self.separator.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self) -> Html {
|
||||||
|
self.inner.into_iter().collect()
|
||||||
|
}
|
||||||
|
}
|
79
src/components/layout/navigation.rs
Normal file
79
src/components/layout/navigation.rs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
use yew::{classes, function_component, html, use_state_eq, AttrValue, Callback, Html, Properties};
|
||||||
|
use yew_icons::{Icon, IconId};
|
||||||
|
use yew_router::prelude::Link;
|
||||||
|
|
||||||
|
use crate::pages::Route;
|
||||||
|
|
||||||
|
fn navigation_link(target: Route, title: AttrValue) -> Html {
|
||||||
|
html! {
|
||||||
|
<Link<Route>
|
||||||
|
classes={classes!(
|
||||||
|
"px-4", "py-6", "transition-colors",
|
||||||
|
"hover:bg-primary-dark", "hover:text-neutral-50"
|
||||||
|
)}
|
||||||
|
to={target}>
|
||||||
|
{title}
|
||||||
|
</Link<Route>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct NavigationProps {}
|
||||||
|
|
||||||
|
#[function_component(Navigation)]
|
||||||
|
pub fn navigation(_: &NavigationProps) -> Html {
|
||||||
|
let open = use_state_eq(|| false);
|
||||||
|
|
||||||
|
let on_open_click = {
|
||||||
|
let open = open.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
open.set(!*open);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<nav class="bg-primary shadow-md text-neutral-200">
|
||||||
|
<div class="container mx-auto flex flex-col md:flex-row px-4 sm:px-0">
|
||||||
|
<div class="flex flex-row justify-between items-center my-4 md:my-0">
|
||||||
|
<Link<Route> classes="block mr-4" to={Route::Home}>
|
||||||
|
<img class="block"
|
||||||
|
src="/media/logo-text.png"
|
||||||
|
width="154"
|
||||||
|
height="28"
|
||||||
|
alt="Blake Rain" />
|
||||||
|
</Link<Route>>
|
||||||
|
<button type="button" class="md:hidden" onclick={on_open_click}>
|
||||||
|
<Icon icon_id={IconId::LucideMenu} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class={classes!("md:flex", "flex-col", "md:flex-row",
|
||||||
|
if *open { "flex" } else { "hidden" })}>
|
||||||
|
{navigation_link(Route::Blog, "Blog".into())}
|
||||||
|
{navigation_link(Route::About, "About".into())}
|
||||||
|
</div>
|
||||||
|
<div class={classes!("hidden", "md:flex", "flex-row",
|
||||||
|
"items-center", "gap-2", "ml-auto")}>
|
||||||
|
<a href="https://github.com/BlakeRain"
|
||||||
|
class="text-neutral-200/75 hover:text-neutral-50"
|
||||||
|
title="GitHub"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer">
|
||||||
|
<Icon icon_id={IconId::BootstrapGithub} height={"1.1em"} />
|
||||||
|
</a>
|
||||||
|
<a href="https://mastodonapp.uk/@BlakeRain"
|
||||||
|
class="text-neutral-200/75 hover:text-neutral-50"
|
||||||
|
title="@BlakeRain@mastodonapp.uk"
|
||||||
|
target="_blank"
|
||||||
|
rel="me noreferrer">
|
||||||
|
<Icon icon_id={IconId::BootstrapMastodon} height={"1.1em"} />
|
||||||
|
</a>
|
||||||
|
<a href="/feeds/feed.xml"
|
||||||
|
class="text-neutral-200/75 hover:text-neutral-50"
|
||||||
|
title="RSS Feed">
|
||||||
|
<Icon icon_id={IconId::LucideRss} height={"1.1em"} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,4 @@
|
|||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod components;
|
||||||
|
pub mod model;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
|
87
src/model.rs
Normal file
87
src/model.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
pub mod frontmatter;
|
||||||
|
pub mod source;
|
||||||
|
pub mod tags;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use self::frontmatter::FrontMatter;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct DocInfo {
|
||||||
|
/// The slug used to form the URL for this document.
|
||||||
|
pub slug: String,
|
||||||
|
/// The rendered title for the document.
|
||||||
|
pub title: String,
|
||||||
|
/// Any given excerpt.
|
||||||
|
pub excerpt: Option<String>,
|
||||||
|
/// The date on which this document was published.
|
||||||
|
pub published: Option<OffsetDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct PostInfo {
|
||||||
|
/// Document information.
|
||||||
|
pub doc_info: DocInfo,
|
||||||
|
/// Tags attached to this post.
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
/// The read time (in seconds).
|
||||||
|
pub reading_time: Option<usize>,
|
||||||
|
/// The URL to the cover image.
|
||||||
|
pub cover_image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Post {
|
||||||
|
/// Information about the post
|
||||||
|
pub info: PostInfo,
|
||||||
|
/// The main content
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostInfo {
|
||||||
|
pub fn from_front_matter(
|
||||||
|
slug: String,
|
||||||
|
reading_time: Option<usize>,
|
||||||
|
excerpt: Option<String>,
|
||||||
|
FrontMatter {
|
||||||
|
title,
|
||||||
|
tags,
|
||||||
|
published,
|
||||||
|
cover,
|
||||||
|
}: FrontMatter,
|
||||||
|
) -> Self {
|
||||||
|
PostInfo {
|
||||||
|
doc_info: DocInfo {
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
excerpt,
|
||||||
|
published,
|
||||||
|
},
|
||||||
|
|
||||||
|
tags,
|
||||||
|
reading_time,
|
||||||
|
cover_image: cover,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct Tag {
|
||||||
|
/// The slug of the tag
|
||||||
|
pub slug: String,
|
||||||
|
/// The display name of the tag
|
||||||
|
pub name: String,
|
||||||
|
/// Whether the tag is visible or not
|
||||||
|
pub visibility: TagVisibility,
|
||||||
|
/// A description of the tag
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub enum TagVisibility {
|
||||||
|
#[serde(rename = "public")]
|
||||||
|
Public,
|
||||||
|
#[serde(rename = "private")]
|
||||||
|
Private,
|
||||||
|
}
|
35
src/model/frontmatter.rs
Normal file
35
src/model/frontmatter.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use gray_matter::{engine::YAML, Matter, ParsedEntity};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use super::source::SourceError;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct FrontMatter {
|
||||||
|
pub title: String,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
#[serde(
|
||||||
|
serialize_with = "time::serde::rfc3339::option::serialize",
|
||||||
|
deserialize_with = "time::serde::rfc3339::option::deserialize"
|
||||||
|
)]
|
||||||
|
pub published: Option<OffsetDateTime>,
|
||||||
|
pub cover: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_front_matter(
|
||||||
|
content: &[u8],
|
||||||
|
) -> Result<(Option<FrontMatter>, ParsedEntity), SourceError> {
|
||||||
|
let content = unsafe { std::str::from_utf8_unchecked(content) };
|
||||||
|
let matter = Matter::<YAML>::new().parse(content);
|
||||||
|
|
||||||
|
let info: Option<FrontMatter> = if let Some(data) = &matter.data {
|
||||||
|
Some(data.deserialize().map_err(|err| {
|
||||||
|
log::error!("Failed to parse front matter: {err:?}");
|
||||||
|
SourceError::InvalidFrontMatter(err.to_string())
|
||||||
|
})?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((info, matter))
|
||||||
|
}
|
86
src/model/source.rs
Normal file
86
src/model/source.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use thiserror::Error;
|
||||||
|
use yew::{function_component, html, Children, ContextProvider, Html, Properties};
|
||||||
|
|
||||||
|
use super::{PostInfo, Tag};
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
mod web;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "hydration"))]
|
||||||
|
mod fs;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum SourceError {
|
||||||
|
#[error("Invalid front matter: {0}")]
|
||||||
|
InvalidFrontMatter(String),
|
||||||
|
#[error("Invalid tags format")]
|
||||||
|
InvalidTags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ModelSource: Send + Sync {
|
||||||
|
async fn get_posts(&self) -> Result<Vec<PostInfo>, SourceError>;
|
||||||
|
async fn get_tags(&self) -> Result<HashMap<String, Tag>, SourceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_source() -> Box<dyn ModelSource> {
|
||||||
|
// If we're compiled to use hydration, we want to use the web source.
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
return Box::new(self::web::Source::new());
|
||||||
|
|
||||||
|
// If we're NOT compiled to use hydration, we want to use the file-system source.
|
||||||
|
#[cfg(not(feature = "hydration"))]
|
||||||
|
return Box::new(fs::Source::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModelSourceWrapper {
|
||||||
|
inner: Arc<Box<dyn ModelSource>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for ModelSourceWrapper {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ModelSourceWrapper {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ModelSource for ModelSourceWrapper {
|
||||||
|
async fn get_posts(&self) -> Result<Vec<PostInfo>, SourceError> {
|
||||||
|
self.inner.get_posts().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_tags(&self) -> Result<HashMap<String, Tag>, SourceError> {
|
||||||
|
self.inner.get_tags().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct ModelProviderProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(ModelProvider)]
|
||||||
|
pub fn model_provider(props: &ModelProviderProps) -> Html {
|
||||||
|
let source = get_source();
|
||||||
|
let source = ModelSourceWrapper {
|
||||||
|
inner: Arc::new(source),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ContextProvider<ModelSourceWrapper> context={source}>
|
||||||
|
{props.children.clone()}
|
||||||
|
</ContextProvider<ModelSourceWrapper>>
|
||||||
|
}
|
||||||
|
}
|
76
src/model/source/fs.rs
Normal file
76
src/model/source/fs.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use include_dir::{include_dir, Dir, File};
|
||||||
|
|
||||||
|
use crate::model::{frontmatter::parse_front_matter, PostInfo, Tag};
|
||||||
|
|
||||||
|
use super::{ModelSource, SourceError};
|
||||||
|
|
||||||
|
pub struct Source {}
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ModelSource for Source {
|
||||||
|
async fn get_posts(&self) -> Result<Vec<PostInfo>, SourceError> {
|
||||||
|
let files = get_post_files().map(|file| -> Result<Option<PostInfo>, SourceError> {
|
||||||
|
let (Some(front_matter), matter) = parse_front_matter(file.contents())? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let slug = file
|
||||||
|
.path()
|
||||||
|
.file_stem()
|
||||||
|
.expect("filename")
|
||||||
|
.to_str()
|
||||||
|
.expect("valid file name")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let reading_time = words_count::count(&matter.content).words / 200;
|
||||||
|
|
||||||
|
Ok(Some(PostInfo::from_front_matter(
|
||||||
|
slug,
|
||||||
|
Some(reading_time),
|
||||||
|
matter.excerpt,
|
||||||
|
front_matter,
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut posts = Vec::new();
|
||||||
|
for file in files {
|
||||||
|
let file = file?;
|
||||||
|
if let Some(file) = file {
|
||||||
|
posts.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts.sort_by(|a, b| b.doc_info.published.cmp(&a.doc_info.published));
|
||||||
|
|
||||||
|
Ok(posts)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_tags(&self) -> Result<HashMap<String, Tag>, SourceError> {
|
||||||
|
let file = CONTENT_DIR.get_file("tags.yaml").expect("tags.yaml");
|
||||||
|
match serde_yaml::from_slice::<Vec<Tag>>(file.contents()) {
|
||||||
|
Ok(tags) => Ok(tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|tag| (tag.slug.clone(), tag))
|
||||||
|
.collect()),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to parse tags.yaml: {err}");
|
||||||
|
Err(SourceError::InvalidTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_post_files<'a>() -> impl Iterator<Item = &'a File<'a>> {
|
||||||
|
CONTENT_DIR.get_dir("posts").expect("posts dir").files()
|
||||||
|
}
|
||||||
|
|
||||||
|
static CONTENT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/content");
|
20
src/model/source/web.rs
Normal file
20
src/model/source/web.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::model::PostInfo;
|
||||||
|
|
||||||
|
use super::{ModelProvider, SourceError};
|
||||||
|
|
||||||
|
pub struct Source {}
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ModelProvider for Source {
|
||||||
|
async fn get_posts(&self) -> Result<Vec<PostInfo>, SourceError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
54
src/model/tags.rs
Normal file
54
src/model/tags.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use yew::{
|
||||||
|
function_component, html, use_context, use_state, Children, ContextProvider, Html, Properties,
|
||||||
|
};
|
||||||
|
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||||
|
|
||||||
|
use crate::model::source::{ModelSource, SourceError};
|
||||||
|
|
||||||
|
use super::{source::ModelSourceWrapper, Tag};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct TagsProviderProps {
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, PartialEq)]
|
||||||
|
pub struct TagsContext {
|
||||||
|
tags: HashMap<String, Tag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagsContext {
|
||||||
|
fn new(tags: HashMap<String, Tag>) -> Self {
|
||||||
|
Self { tags }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tag<S: AsRef<str>>(&self, slug: S) -> Option<Tag> {
|
||||||
|
self.tags.get(slug.as_ref()).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(TagsProvider)]
|
||||||
|
pub fn tags_provider(props: &TagsProviderProps) -> Html {
|
||||||
|
let source = use_context::<ModelSourceWrapper>().expect("ModelSource to be provided");
|
||||||
|
let tags = use_state(TagsContext::default);
|
||||||
|
|
||||||
|
{
|
||||||
|
let tags = tags.clone();
|
||||||
|
use_async_with_options::<_, (), SourceError>(
|
||||||
|
async move {
|
||||||
|
tags.set(TagsContext::new(source.get_tags().await?));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
UseAsyncOptions::enable_auto(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ContextProvider<TagsContext> context={(*tags).clone()}>
|
||||||
|
{props.children.clone()}
|
||||||
|
</ContextProvider<TagsContext>>
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,10 @@ pub enum Route {
|
|||||||
BlogPost { slug: String },
|
BlogPost { slug: String },
|
||||||
#[at("/disclaimer")]
|
#[at("/disclaimer")]
|
||||||
Disclaimer,
|
Disclaimer,
|
||||||
|
#[at("/tags")]
|
||||||
|
Tags,
|
||||||
|
#[at("/tags/{slug}")]
|
||||||
|
Tag { slug: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
@ -29,6 +33,8 @@ impl Route {
|
|||||||
Self::Blog => html! { <blog::Page /> },
|
Self::Blog => html! { <blog::Page /> },
|
||||||
Self::BlogPost { slug } => html! { <blog_post::Page slug={slug} /> },
|
Self::BlogPost { slug } => html! { <blog_post::Page slug={slug} /> },
|
||||||
Self::Disclaimer => html! { <disclaimer::Page /> },
|
Self::Disclaimer => html! { <disclaimer::Page /> },
|
||||||
|
Self::Tags => unimplemented!(),
|
||||||
|
Self::Tag { .. } => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,30 @@
|
|||||||
use yew::{function_component, html, Html};
|
use yew::{function_component, html, use_context, use_state, Html};
|
||||||
|
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::blog::post_card_list::PostCardList,
|
||||||
|
model::source::{ModelSource, ModelSourceWrapper, SourceError},
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page() -> Html {
|
pub fn page() -> Html {
|
||||||
html! { "Blog" }
|
let source = use_context::<ModelSourceWrapper>().expect("ModelSource to be provided");
|
||||||
|
let posts = use_state(Vec::new);
|
||||||
|
|
||||||
|
{
|
||||||
|
let posts = posts.clone();
|
||||||
|
use_async_with_options::<_, (), SourceError>(
|
||||||
|
async move {
|
||||||
|
posts.set(source.get_posts().await?);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
UseAsyncOptions::enable_auto(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<PostCardList posts={(*posts).clone()} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,30 @@
|
|||||||
use yew::{function_component, html, use_state, Callback, Html};
|
use yew::{function_component, html, use_context, use_state, Html};
|
||||||
use yew_router::prelude::Link;
|
use yew_hooks::{use_async_with_options, UseAsyncOptions};
|
||||||
|
|
||||||
use crate::pages::Route;
|
use crate::{
|
||||||
|
components::blog::post_card_list::PostCardList,
|
||||||
|
model::source::{ModelSource, ModelSourceWrapper, SourceError},
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page() -> Html {
|
pub fn page() -> Html {
|
||||||
let count = use_state(|| 0);
|
let source = use_context::<ModelSourceWrapper>().expect("ModelSource to be provided");
|
||||||
|
let posts = use_state(Vec::new);
|
||||||
|
|
||||||
let button_click = {
|
{
|
||||||
let count = count.clone();
|
let posts = posts.clone();
|
||||||
Callback::from(move |_| count.set(*count + 1))
|
use_async_with_options::<_, (), SourceError>(
|
||||||
};
|
async move {
|
||||||
|
posts.set(source.get_posts().await?);
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
UseAsyncOptions::enable_auto(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<h1>{"Home"}</h1>
|
<PostCardList posts={(*posts).clone()} />
|
||||||
<button type="button" onclick={button_click}>{format!("Clicked {} times", *count)}</button>
|
|
||||||
<Link<Route> to={Route::About}>{"About"}</Link<Route>>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
@import "tailwind.css";
|
@import "tailwind.css";
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
body {
|
||||||
|
@apply bg-white text-neutral-800;
|
||||||
|
@apply dark:bg-zinc-900 dark:text-neutral-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
text: ["Noto Serif", "serif"],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
dark: "#0D1B2A",
|
dark: "#22293D",
|
||||||
DEFAULT: "#1B263B",
|
DEFAULT: "#13304E",
|
||||||
light: "#415A77",
|
|
||||||
lighter: "#778DA9",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user