Switch over to WebAssembly, Rust and Yew #35
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1402,8 +1402,10 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-logger",
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yew-hooks",
|
||||
"yew-router",
|
||||
|
13
Cargo.toml
13
Cargo.toml
@ -34,6 +34,7 @@ reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0" }
|
||||
thiserror = { version = "1.0" }
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
yew = { version = "0.20" }
|
||||
yew-hooks = { version = "0.2" }
|
||||
yew-router = { version = "0.17" }
|
||||
@ -61,6 +62,18 @@ features = [
|
||||
"LucideRss"
|
||||
]
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Document",
|
||||
"DomRect",
|
||||
"Element",
|
||||
"IntersectionObserver",
|
||||
"IntersectionObserverEntry",
|
||||
"ScrollToOptions",
|
||||
"Window"
|
||||
]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = { version = "0.4" }
|
||||
wasm-logger = { version = "0.2" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
use yew::{function_component, html, Children, Html, Properties};
|
||||
|
||||
mod footer;
|
||||
pub mod goto_top;
|
||||
pub mod intersperse;
|
||||
mod navigation;
|
||||
|
||||
|
@ -12,7 +12,7 @@ pub fn footer(_: &FooterProps) -> Html {
|
||||
let year = OffsetDateTime::now_utc().year();
|
||||
|
||||
html! {
|
||||
<div class="bg-primary text-neutral-400 text-sm mt-4">
|
||||
<footer 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}")}
|
||||
@ -44,7 +44,7 @@ pub fn footer(_: &FooterProps) -> Html {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
}
|
||||
|
||||
|
95
src/components/layout/goto_top.rs
Normal file
95
src/components/layout/goto_top.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||
use web_sys::{window, Element, IntersectionObserver, IntersectionObserverEntry, ScrollToOptions};
|
||||
use yew::{classes, function_component, html, use_effect, use_state, use_state_eq, Callback, Html};
|
||||
|
||||
#[function_component(GotoTop)]
|
||||
pub fn goto_top() -> Html {
|
||||
let footer_el = use_state_eq(|| None::<Element>);
|
||||
let visible = use_state_eq(|| false);
|
||||
let footer_visible = use_state_eq(|| false);
|
||||
|
||||
let class = classes!(
|
||||
"cursor-pointer",
|
||||
"py-4",
|
||||
"px-8",
|
||||
"border",
|
||||
"border-primary-dark",
|
||||
"bg-primary-dark",
|
||||
"hover:bg-primary",
|
||||
"fixed",
|
||||
"bottom-8",
|
||||
"right-8",
|
||||
"transition-all",
|
||||
"transition-200",
|
||||
if *visible { "opacity-100" } else { "opacity-0" },
|
||||
);
|
||||
|
||||
let style = if *footer_visible {
|
||||
let height = if let Some(footer) = &*footer_el {
|
||||
-footer.get_bounding_client_rect().height()
|
||||
} else {
|
||||
-10.0
|
||||
};
|
||||
|
||||
format!("transform: translateY({}px)", height)
|
||||
} else if *visible {
|
||||
"transform: translateY(0px)".to_string()
|
||||
} else {
|
||||
"transform: translateY(100px)".to_string()
|
||||
};
|
||||
|
||||
let observe = {
|
||||
Closure::<dyn Fn(Vec<IntersectionObserverEntry>)>::wrap(Box::new(
|
||||
move |entries: Vec<IntersectionObserverEntry>| {
|
||||
for entry in entries {
|
||||
let tag_name = entry.target().tag_name();
|
||||
if tag_name == "NAV" {
|
||||
log::info!("NAV visible: {}", entry.is_intersecting());
|
||||
visible.set(!entry.is_intersecting());
|
||||
} else if tag_name == "FOOTER" {
|
||||
log::info!("FOOTER visible: {}", entry.is_intersecting());
|
||||
footer_visible.set(entry.is_intersecting());
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
};
|
||||
|
||||
{
|
||||
use_effect(move || {
|
||||
let document = window().expect("window").document().expect("document");
|
||||
let header = document
|
||||
.query_selector("nav:first-of-type")
|
||||
.expect("query_selector");
|
||||
let footer = document.query_selector("footer").expect("query_selector");
|
||||
|
||||
let observer = IntersectionObserver::new(observe.as_ref().unchecked_ref()).unwrap();
|
||||
|
||||
if let Some(header) = header {
|
||||
observer.observe(&header);
|
||||
}
|
||||
|
||||
if let Some(footer) = footer {
|
||||
observer.observe(&footer);
|
||||
footer_el.set(Some(footer));
|
||||
}
|
||||
|
||||
move || {
|
||||
observer.disconnect();
|
||||
drop(observe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let onclick = Callback::from(move |_| {
|
||||
let mut opts = ScrollToOptions::new();
|
||||
opts.top(0f64);
|
||||
window()
|
||||
.expect("window")
|
||||
.scroll_to_with_scroll_to_options(&opts);
|
||||
});
|
||||
|
||||
html! {
|
||||
<button {class} {style} type="button" tabindex="-1" {onclick}>{"↑ Goto Top"}</button>
|
||||
}
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
use yew::{function_component, html, Html, Properties};
|
||||
|
||||
use crate::{components::content::PostContent, model::ProvideTags};
|
||||
use crate::{
|
||||
components::{content::PostContent, layout::goto_top::GotoTop},
|
||||
model::ProvideTags,
|
||||
};
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct PageProps {
|
||||
@ -23,6 +26,7 @@ pub fn page(props: &PageProps) -> Html {
|
||||
html! {
|
||||
<ProvideTags>
|
||||
<PostContent details={details} content={content} />
|
||||
<GotoTop />
|
||||
</ProvideTags>
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc;
|
||||
@apply list-disc pl-10;
|
||||
}
|
||||
|
||||
figure {
|
||||
|
Loading…
Reference in New Issue
Block a user