Switch over to WebAssembly, Rust and Yew #35
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1402,8 +1402,10 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-logger",
|
"wasm-logger",
|
||||||
|
"web-sys",
|
||||||
"yew",
|
"yew",
|
||||||
"yew-hooks",
|
"yew-hooks",
|
||||||
"yew-router",
|
"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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
thiserror = { version = "1.0" }
|
thiserror = { version = "1.0" }
|
||||||
|
wasm-bindgen = { version = "0.2" }
|
||||||
yew = { version = "0.20" }
|
yew = { version = "0.20" }
|
||||||
yew-hooks = { version = "0.2" }
|
yew-hooks = { version = "0.2" }
|
||||||
yew-router = { version = "0.17" }
|
yew-router = { version = "0.17" }
|
||||||
@ -61,6 +62,18 @@ features = [
|
|||||||
"LucideRss"
|
"LucideRss"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3"
|
||||||
|
features = [
|
||||||
|
"Document",
|
||||||
|
"DomRect",
|
||||||
|
"Element",
|
||||||
|
"IntersectionObserver",
|
||||||
|
"IntersectionObserverEntry",
|
||||||
|
"ScrollToOptions",
|
||||||
|
"Window"
|
||||||
|
]
|
||||||
|
|
||||||
[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" }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use yew::{function_component, html, Children, Html, Properties};
|
use yew::{function_component, html, Children, Html, Properties};
|
||||||
|
|
||||||
mod footer;
|
mod footer;
|
||||||
|
pub mod goto_top;
|
||||||
pub mod intersperse;
|
pub mod intersperse;
|
||||||
mod navigation;
|
mod navigation;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ pub fn footer(_: &FooterProps) -> Html {
|
|||||||
let year = OffsetDateTime::now_utc().year();
|
let year = OffsetDateTime::now_utc().year();
|
||||||
|
|
||||||
html! {
|
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 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>
|
<div>
|
||||||
{format!("Blake Rain © {year}")}
|
{format!("Blake Rain © {year}")}
|
||||||
@ -44,7 +44,7 @@ pub fn footer(_: &FooterProps) -> Html {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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 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)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PageProps {
|
pub struct PageProps {
|
||||||
@ -23,6 +26,7 @@ pub fn page(props: &PageProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<PostContent details={details} content={content} />
|
<PostContent details={details} content={content} />
|
||||||
|
<GotoTop />
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@apply list-disc;
|
@apply list-disc pl-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure {
|
figure {
|
||||||
|
Loading…
Reference in New Issue
Block a user