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"
|
||||
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]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@ -145,6 +156,12 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.2"
|
||||
@ -433,12 +450,29 @@ dependencies = [
|
||||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.2"
|
||||
@ -477,7 +511,26 @@ version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f"
|
||||
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]]
|
||||
@ -487,7 +540,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@ -522,6 +585,12 @@ version = "0.2.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.5"
|
||||
@ -880,6 +949,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
@ -893,13 +975,22 @@ dependencies = [
|
||||
name = "site"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"env_logger",
|
||||
"gray_matter",
|
||||
"include_dir",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"words-count",
|
||||
"yew",
|
||||
"yew-hooks",
|
||||
"yew-router",
|
||||
"yew_icons",
|
||||
]
|
||||
@ -988,6 +1079,7 @@ checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"serde",
|
||||
@ -1083,12 +1175,24 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-blocks"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c84398c527c802fbf222e5145f220382d60f1878e0e6cb4d22a3080949a8ddcd"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-width"
|
||||
version = "0.1.6"
|
||||
@ -1291,6 +1395,24 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "yew"
|
||||
version = "0.20.0"
|
||||
@ -1304,7 +1426,7 @@ dependencies = [
|
||||
"gloo",
|
||||
"html-escape",
|
||||
"implicit-clone",
|
||||
"indexmap",
|
||||
"indexmap 1.9.3",
|
||||
"js-sys",
|
||||
"prokio",
|
||||
"rustversion",
|
||||
@ -1319,6 +1441,22 @@ dependencies = [
|
||||
"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]]
|
||||
name = "yew-macro"
|
||||
version = "0.20.0"
|
||||
|
22
Cargo.toml
22
Cargo.toml
@ -23,9 +23,18 @@ required-features = [
|
||||
name = "site"
|
||||
|
||||
[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-hooks = { version = "0.2" }
|
||||
yew-router = { version = "0.17" }
|
||||
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]
|
||||
version = "0.3"
|
||||
@ -34,20 +43,19 @@ features = [
|
||||
"local-offset",
|
||||
"macros",
|
||||
"parsing",
|
||||
"serde"
|
||||
"serde",
|
||||
"wasm-bindgen"
|
||||
]
|
||||
|
||||
[dependencies.yew_icons]
|
||||
version = "0.7"
|
||||
features = [
|
||||
"BootstrapDot",
|
||||
"BootstrapGithub",
|
||||
"BootstrapMastodon",
|
||||
"LucideMenu",
|
||||
"LucideRss"
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")']
|
||||
required-features = [
|
||||
"time/wasm-bindgen"
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
wasm-logger = { version = "0.2" }
|
||||
|
@ -2,141 +2,118 @@
|
||||
# tags.yaml
|
||||
#
|
||||
|
||||
getting-started:
|
||||
slug: getting-started
|
||||
- slug: getting-started
|
||||
name: Getting Started
|
||||
visibility: public
|
||||
|
||||
haskell:
|
||||
slug: haskell
|
||||
- slug: haskell
|
||||
name: Haskell
|
||||
visibility: public
|
||||
description: Posts that feature the Haskell programming language.
|
||||
|
||||
reviews:
|
||||
slug: reviews
|
||||
- slug: reviews
|
||||
name: Reviews
|
||||
visibility: public
|
||||
description: Reviews of books or other articles
|
||||
|
||||
electronics:
|
||||
slug: electronics
|
||||
- slug: electronics
|
||||
name: Electronics
|
||||
visibility: public
|
||||
description: Posts that feature electronic circuits.
|
||||
|
||||
javascript:
|
||||
slug: javascript
|
||||
- slug: javascript
|
||||
name: JavaScript
|
||||
visibility: public
|
||||
description: Posts that feature the JavaScript scripting language.
|
||||
|
||||
python:
|
||||
slug: python
|
||||
- slug: python
|
||||
name: Python
|
||||
visibility: public
|
||||
description: Posts that feature the Python scripting language
|
||||
|
||||
ghost-tag:
|
||||
slug: ghost-tag
|
||||
- slug: ghost-tag
|
||||
name: Ghost
|
||||
visibility: public
|
||||
description: Posts related to my use of the Ghost CMS
|
||||
|
||||
gtk:
|
||||
slug: gtk
|
||||
- slug: gtk
|
||||
name: GTK
|
||||
visibility: public
|
||||
description: Posts that feature the GTK graphical user interface toolkit.
|
||||
|
||||
aws:
|
||||
slug: aws
|
||||
- slug: aws
|
||||
name: AWS
|
||||
visibility: public
|
||||
description: Posts that relate to Amazon Web Services
|
||||
|
||||
linux:
|
||||
slug: linux
|
||||
- slug: linux
|
||||
name: Linux
|
||||
visibility: public
|
||||
description: Posts that feature the Linux operating system.
|
||||
|
||||
cairo:
|
||||
slug: cairo
|
||||
- slug: cairo
|
||||
name: Cairo
|
||||
visibility: public
|
||||
description: Posts that feature the Cairo vector-based graphics library.
|
||||
|
||||
cpp:
|
||||
slug: cpp
|
||||
- slug: cpp
|
||||
name: C++
|
||||
visibility: public
|
||||
description: Posts that feature the C++ programming language.
|
||||
|
||||
yesod:
|
||||
slug: yesod
|
||||
- slug: yesod
|
||||
name: Yesod
|
||||
visibility: public
|
||||
description:
|
||||
Posts which feature the Yesod web application development framework
|
||||
for the Haskell programming language.
|
||||
|
||||
ethernet:
|
||||
slug: ethernet
|
||||
- slug: ethernet
|
||||
name: Ethernet
|
||||
visibility: public
|
||||
description: Posts that relate to Ethernet networking
|
||||
|
||||
react:
|
||||
slug: react
|
||||
- slug: react
|
||||
name: React
|
||||
visibility: public
|
||||
description: Posts that make use of the React JavaScript library.
|
||||
|
||||
gc:
|
||||
slug: gc
|
||||
- slug: gc
|
||||
name: GC
|
||||
visibility: public
|
||||
description: Posts that relate to garbage collection and memory management.
|
||||
|
||||
sqlite:
|
||||
slug: sqlite
|
||||
- slug: sqlite
|
||||
name: SQLite
|
||||
visibility: public
|
||||
description: Posts that feature the SQLite embedded relational database.
|
||||
|
||||
chakracore:
|
||||
slug: chakracore
|
||||
- slug: chakracore
|
||||
name: ChakraCore
|
||||
visibility: public
|
||||
description: Posts that feature the ChakraCore JavaScript engine.
|
||||
|
||||
pci:
|
||||
slug: pci
|
||||
- slug: pci
|
||||
name: PCI
|
||||
visibility: public
|
||||
description: Articles related to programming PCI express devices.
|
||||
|
||||
usb:
|
||||
slug: usb
|
||||
- slug: usb
|
||||
name: USB
|
||||
visibility: public
|
||||
description: Posts that relate to working with the Universal Serial Bus
|
||||
|
||||
intel-i350:
|
||||
slug: intel-i350
|
||||
- slug: intel-i350
|
||||
name: Intel i350
|
||||
visibility: public
|
||||
description: Blog posts about programming the Intel i350 NIC and related models.
|
||||
|
||||
rust:
|
||||
slug: rust
|
||||
- slug: rust
|
||||
name: Rust
|
||||
visibility: public
|
||||
description: Blog posts related to the Rust programming language
|
||||
|
||||
mastodon:
|
||||
slug: mastodon
|
||||
- slug: mastodon
|
||||
name: Mastodon
|
||||
visibility: public
|
||||
description: Posts that feature the Mastodon social network.
|
||||
|
@ -4,7 +4,15 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Blake Rain</title>
|
||||
<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="css" href="target/main.css" />
|
||||
<link data-trunk rel="copy-dir" href="public/media" />
|
||||
<link data-trunk rel="copy-dir" href="public/content" />
|
||||
</head>
|
||||
</html>
|
||||
|
18
src/app.rs
18
src/app.rs
@ -10,14 +10,24 @@ use yew_router::{
|
||||
#[cfg(not(feature = "static"))]
|
||||
use yew_router::BrowserRouter;
|
||||
|
||||
use crate::pages::Route;
|
||||
use crate::{
|
||||
components::layout::Layout,
|
||||
model::{source::ModelProvider, tags::TagsProvider},
|
||||
pages::Route,
|
||||
};
|
||||
|
||||
#[function_component(AppContent)]
|
||||
fn app_content() -> Html {
|
||||
html! {
|
||||
<main>
|
||||
<Switch<Route> render={Route::switch} />
|
||||
</main>
|
||||
<ModelProvider>
|
||||
<TagsProvider>
|
||||
<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 components;
|
||||
pub mod model;
|
||||
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 },
|
||||
#[at("/disclaimer")]
|
||||
Disclaimer,
|
||||
#[at("/tags")]
|
||||
Tags,
|
||||
#[at("/tags/{slug}")]
|
||||
Tag { slug: String },
|
||||
}
|
||||
|
||||
impl Route {
|
||||
@ -29,6 +33,8 @@ impl Route {
|
||||
Self::Blog => html! { <blog::Page /> },
|
||||
Self::BlogPost { slug } => html! { <blog_post::Page slug={slug} /> },
|
||||
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)]
|
||||
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_router::prelude::Link;
|
||||
use yew::{function_component, html, use_context, use_state, Html};
|
||||
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)]
|
||||
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();
|
||||
Callback::from(move |_| count.set(*count + 1))
|
||||
};
|
||||
{
|
||||
let posts = posts.clone();
|
||||
use_async_with_options::<_, (), SourceError>(
|
||||
async move {
|
||||
posts.set(source.get_posts().await?);
|
||||
Ok(())
|
||||
},
|
||||
UseAsyncOptions::enable_auto(),
|
||||
);
|
||||
}
|
||||
|
||||
html! {
|
||||
<>
|
||||
<h1>{"Home"}</h1>
|
||||
<button type="button" onclick={button_click}>{format!("Clicked {} times", *count)}</button>
|
||||
<Link<Route> to={Route::About}>{"About"}</Link<Route>>
|
||||
<PostCardList posts={(*posts).clone()} />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
@import "tailwind.css";
|
||||
|
||||
@layer components {
|
||||
body {
|
||||
@apply bg-white text-neutral-800;
|
||||
@apply dark:bg-zinc-900 dark:text-neutral-200;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
text: ["Noto Serif", "serif"],
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
dark: "#0D1B2A",
|
||||
DEFAULT: "#1B263B",
|
||||
light: "#415A77",
|
||||
lighter: "#778DA9",
|
||||
dark: "#22293D",
|
||||
DEFAULT: "#13304E",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user