upgrading hackernews example
This commit is contained in:
parent
32294d6cab
commit
4ffa3c46b6
|
@ -8,6 +8,8 @@ crate-type = ["cdylib", "rlib"]
|
|||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
|
@ -15,7 +17,7 @@ actix-files = { version = "0.6", optional = true }
|
|||
actix-web = { version = "4", optional = true, features = ["macros"] }
|
||||
console_log = "1"
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { path = "../../leptos" }
|
||||
leptos = { path = "../../leptos", features = ["tracing"] }
|
||||
leptos_meta = { path = "../../meta" }
|
||||
leptos_actix = { path = "../../integrations/actix", optional = true }
|
||||
leptos_router = { path = "../../router" }
|
||||
|
@ -27,18 +29,15 @@ tracing = "0.1"
|
|||
# openssl = { version = "0.10", features = ["v110"] }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["AbortController", "AbortSignal"] }
|
||||
send_wrapper = "0.6.0"
|
||||
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber-wasm = "0.1"
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
"dep:leptos_actix",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = ["dep:actix-files", "dep:actix-web", "dep:leptos_actix", "leptos/ssr"]
|
||||
|
||||
[profile.wasm-release]
|
||||
inherits = "release"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use leptos::Serializable;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn story(path: &str) -> String {
|
||||
|
@ -10,46 +10,52 @@ pub fn user(path: &str) -> String {
|
|||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
pub fn fetch_api<T>(
|
||||
path: &str,
|
||||
) -> impl std::future::Future<Output = Option<T>> + Send + '_
|
||||
where
|
||||
T: Serializable,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let abort_controller = web_sys::AbortController::new().ok();
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
use send_wrapper::SendWrapper;
|
||||
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
leptos::on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
SendWrapper::new(async move {
|
||||
use leptos::reactive_graph::owner::Owner;
|
||||
|
||||
let json = gloo_net::http::Request::get(path)
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
let abort_controller =
|
||||
SendWrapper::new(web_sys::AbortController::new().ok());
|
||||
let abort_signal = abort_controller.as_ref().map(|a| a.signal());
|
||||
|
||||
T::de(&json).ok()
|
||||
// abort in-flight requests if, e.g., we've navigated away from this page
|
||||
Owner::on_cleanup(move || {
|
||||
if let Some(abort_controller) = abort_controller.take() {
|
||||
abort_controller.abort()
|
||||
}
|
||||
});
|
||||
|
||||
gloo_net::http::Request::get(path)
|
||||
.abort_signal(abort_signal.as_ref())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.json()
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||
where
|
||||
T: Serializable,
|
||||
T: Serialize + DeserializeOwned,
|
||||
{
|
||||
let json = reqwest::get(path)
|
||||
reqwest::get(path)
|
||||
.await
|
||||
.map_err(|e| log::error!("{e}"))
|
||||
.ok()?
|
||||
.text()
|
||||
.json()
|
||||
.await
|
||||
.ok()?;
|
||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
||||
.ok()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
mod api;
|
||||
mod routes;
|
||||
use leptos_meta::{provide_meta_context, Link, Meta, Stylesheet};
|
||||
use leptos_router::{
|
||||
components::{FlatRoutes, Route, Router},
|
||||
ParamSegment, StaticSegment,
|
||||
};
|
||||
use routes::{nav::*, stories::*, story::*, users::*};
|
||||
|
||||
#[component]
|
||||
|
@ -15,18 +18,19 @@ pub fn App() -> impl IntoView {
|
|||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
// adding `set_is_routing` causes the router to wait for async data to load on new pages
|
||||
<Router set_is_routing>
|
||||
<Router> // TODO set_is_routing>
|
||||
// shows a progress bar while async data are loading
|
||||
<div class="routing-progress">
|
||||
// TODO
|
||||
/*<div class="routing-progress">
|
||||
<RoutingProgress is_routing max_time=std::time::Duration::from_millis(250)/>
|
||||
</div>
|
||||
</div>*/
|
||||
<Nav />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="users/:id" view=User/>
|
||||
<Route path="stories/:id" view=Story/>
|
||||
<Route path=":stories?" view=Stories/>
|
||||
</Routes>
|
||||
<FlatRoutes fallback=|| "Not found.">
|
||||
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||
<Route path=ParamSegment("stories") view=Stories/>
|
||||
</FlatRoutes>
|
||||
</main>
|
||||
</Router>
|
||||
}
|
||||
|
|
|
@ -50,7 +50,20 @@ async fn main() -> std::io::Result<()> {
|
|||
fn main() {
|
||||
use hackernews::App;
|
||||
|
||||
_ = console_log::init_with_level(log::Level::Debug);
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber_wasm::MakeConsoleWriter;
|
||||
|
||||
fmt()
|
||||
.with_writer(
|
||||
// To avoide trace events in the browser from showing their
|
||||
// JS backtrace, which is very annoying, in my opinion
|
||||
MakeConsoleWriter::default()
|
||||
.map_trace_level_to(tracing::Level::DEBUG),
|
||||
)
|
||||
// For some reason, if we don't do this in the browser, we get
|
||||
// a runtime error.
|
||||
.without_time()
|
||||
.init();
|
||||
console_error_panic_hook::set_once();
|
||||
leptos::mount_to_body(App)
|
||||
leptos::mount::mount_to_body(App)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use leptos::{component, view, IntoView};
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::components::A;
|
||||
|
||||
#[component]
|
||||
pub fn Nav() -> impl IntoView {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::{
|
||||
components::A,
|
||||
hooks::{use_params_map, use_query_map},
|
||||
};
|
||||
|
||||
fn category(from: &str) -> &'static str {
|
||||
match from {
|
||||
|
@ -18,15 +21,19 @@ pub fn Stories() -> impl IntoView {
|
|||
let params = use_params_map();
|
||||
let page = move || {
|
||||
query
|
||||
.with(|q| q.get("page").and_then(|page| page.parse::<usize>().ok()))
|
||||
.read()
|
||||
.get("page")
|
||||
.and_then(|page| page.parse::<usize>().ok())
|
||||
.unwrap_or(1)
|
||||
};
|
||||
let story_type = move || {
|
||||
params
|
||||
.with(|p| p.get("stories").cloned())
|
||||
.read()
|
||||
.get("stories")
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| "top".to_string())
|
||||
};
|
||||
let stories = create_resource(
|
||||
let stories = Resource::new_serde(
|
||||
move || (page(), story_type()),
|
||||
move |(page, story_type)| async move {
|
||||
let path = format!("{}?page={}", category(&story_type), page);
|
||||
|
@ -36,8 +43,9 @@ pub fn Stories() -> impl IntoView {
|
|||
let (pending, set_pending) = create_signal(false);
|
||||
|
||||
let hide_more_link = move || {
|
||||
stories.get().unwrap_or(None).unwrap_or_default().len() < 28
|
||||
|| pending.get()
|
||||
Suspend(async move {
|
||||
stories.await.unwrap_or_default().len() < 28 || pending.get()
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
|
@ -45,27 +53,28 @@ pub fn Stories() -> impl IntoView {
|
|||
<div class="news-list-nav">
|
||||
<span>
|
||||
{move || if page() > 1 {
|
||||
view! {
|
||||
Either::Left(view! {
|
||||
<a class="page-link"
|
||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||
attr:aria_label="Previous Page"
|
||||
aria-label="Previous Page"
|
||||
>
|
||||
"< prev"
|
||||
</a>
|
||||
}.into_any()
|
||||
})
|
||||
} else {
|
||||
view! {
|
||||
Either::Right(view! {
|
||||
<span class="page-link disabled" aria-hidden="true">
|
||||
"< prev"
|
||||
</span>
|
||||
}.into_any()
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span>"page " {page}</span>
|
||||
<Suspense>
|
||||
<span class="page-link"
|
||||
class:disabled=hide_more_link
|
||||
aria-hidden=hide_more_link
|
||||
// TODO support Suspense in attributes
|
||||
/*class:disabled=Suspend(hide_more_link)
|
||||
aria-hidden=Suspend(hide_more_link)*/
|
||||
>
|
||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||
aria-label="Next Page"
|
||||
|
@ -79,25 +88,19 @@ pub fn Stories() -> impl IntoView {
|
|||
<div>
|
||||
<Transition
|
||||
fallback=move || view! { <p>"Loading..."</p> }
|
||||
set_pending
|
||||
// TODO set_pending on Transition
|
||||
//set_pending
|
||||
>
|
||||
{move || match stories.get() {
|
||||
None => None,
|
||||
Some(None) => Some(view! { <p>"Error loading stories."</p> }.into_any()),
|
||||
Some(Some(stories)) => {
|
||||
Some(view! {
|
||||
{move || Suspend(async move { match stories.await {
|
||||
None => Either::Left(view! { <p>"Error loading stories."</p> }),
|
||||
Some(stories) => {
|
||||
Either::Right(view! {
|
||||
<ul>
|
||||
<For
|
||||
each=move || stories.clone()
|
||||
key=|story| story.id
|
||||
let:story
|
||||
>
|
||||
<Story story/>
|
||||
</For>
|
||||
{stories.into_iter().map(|story| view! { <Story story/> }).collect::<Vec<_>>()}
|
||||
</ul>
|
||||
}.into_any())
|
||||
})
|
||||
}
|
||||
}}
|
||||
}})}
|
||||
</Transition>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -112,23 +115,23 @@ fn Story(story: api::Story) -> impl IntoView {
|
|||
<span class="score">{story.points}</span>
|
||||
<span class="title">
|
||||
{if !story.url.starts_with("item?id=") {
|
||||
view! {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
<a href=story.url target="_blank" rel="noreferrer">
|
||||
{story.title.clone()}
|
||||
</a>
|
||||
<span class="host">"("{story.domain}")"</span>
|
||||
</span>
|
||||
}.into_view()
|
||||
})
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <A href=format!("/stories/{}", story.id)>{title.clone()}</A> }.into_view()
|
||||
Either::Right(view! { <A href=format!("/stories/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
</span>
|
||||
<br />
|
||||
<span class="meta">
|
||||
{if story.story_type != "job" {
|
||||
view! {
|
||||
Either::Left(view! {
|
||||
<span>
|
||||
{"by "}
|
||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||
|
@ -141,10 +144,10 @@ fn Story(story: api::Story) -> impl IntoView {
|
|||
}}
|
||||
</A>
|
||||
</span>
|
||||
}.into_view()
|
||||
})
|
||||
} else {
|
||||
let title = story.title.clone();
|
||||
view! { <A href=format!("/item/{}", story.id)>{title.clone()}</A> }.into_view()
|
||||
Either::Right(view! { <A href=format!("/item/{}", story.id)>{title}</A> })
|
||||
}}
|
||||
</span>
|
||||
{(story.story_type != "link").then(|| view! {
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
use crate::api;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos::either::Either;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::Meta;
|
||||
use leptos_router::components::A;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
#[component]
|
||||
pub fn Story() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let story = create_resource(
|
||||
move || params.get().get("id").cloned().unwrap_or_default(),
|
||||
let story = Resource::new_serde(
|
||||
move || {
|
||||
params
|
||||
.get()
|
||||
.get("id")
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default()
|
||||
},
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
None
|
||||
|
@ -17,19 +25,13 @@ pub fn Story() -> impl IntoView {
|
|||
}
|
||||
},
|
||||
);
|
||||
let meta_description = move || {
|
||||
story
|
||||
.get()
|
||||
.and_then(|story| story.map(|story| story.title))
|
||||
.unwrap_or_else(|| "Loading story...".to_string())
|
||||
};
|
||||
|
||||
view! {
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<Meta name="description" content=meta_description/>
|
||||
{move || story.get().map(|story| match story {
|
||||
None => view! { <div class="item-view">"Error loading this story."</div> },
|
||||
Some(story) => view! {
|
||||
Suspense(SuspenseProps::builder().fallback(|| "Loading...").children(ToChildren::to_children(move || Suspend(async move {
|
||||
match story.await {
|
||||
None => Either::Left("Story not found."),
|
||||
Some(story) => {
|
||||
Either::Right(view! {
|
||||
<Meta name="description" content=story.title.clone()/>
|
||||
<div class="item-view">
|
||||
<div class="item-view-header">
|
||||
<a href=story.url target="_blank">
|
||||
|
@ -46,27 +48,28 @@ pub fn Story() -> impl IntoView {
|
|||
</p>})}
|
||||
</div>
|
||||
<div class="item-view-comments">
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
<p class="item-view-comments-header">
|
||||
{if story.comments_count.unwrap_or_default() > 0 {
|
||||
format!("{} comments", story.comments_count.unwrap_or_default())
|
||||
} else {
|
||||
"No comments yet.".into()
|
||||
}}
|
||||
</p>
|
||||
<ul class="comment-children">
|
||||
<For
|
||||
each=move || story.comments.clone().unwrap_or_default()
|
||||
key=|comment| comment.id
|
||||
let:comment
|
||||
>
|
||||
<Comment comment />
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}})}
|
||||
</Suspense>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}))).build())
|
||||
}
|
||||
|
||||
#[component]
|
||||
|
@ -113,7 +116,7 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
|
|||
}
|
||||
})}
|
||||
</li>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
|
||||
fn pluralize(n: usize) -> &'static str {
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
use crate::api::{self, User};
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use leptos::server::Resource;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
use leptos_router::{hooks::use_params_map, *};
|
||||
|
||||
#[component]
|
||||
pub fn User() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let user = create_resource(
|
||||
move || params.get().get("id").cloned().unwrap_or_default(),
|
||||
let user = Resource::new_serde(
|
||||
move || {
|
||||
params
|
||||
.read()
|
||||
.get("id")
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default()
|
||||
},
|
||||
move |id| async move {
|
||||
if id.is_empty() {
|
||||
None
|
||||
|
@ -18,11 +25,11 @@ pub fn User() -> impl IntoView {
|
|||
view! {
|
||||
<div class="user-view">
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
{move || user.get().map(|user| match user {
|
||||
None => view! { <h1>"User not found."</h1> }.into_view(),
|
||||
Some(user) => view! {
|
||||
{move || Suspend(async move { match user.await {
|
||||
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||
Some(user) => Either::Right(view! {
|
||||
<div>
|
||||
<h1>"User: " {&user.id}</h1>
|
||||
<h1>"User: " {user.id.clone()}</h1>
|
||||
<ul class="meta">
|
||||
<li>
|
||||
<span class="label">"Created: "</span> {user.created}
|
||||
|
@ -38,8 +45,8 @@ pub fn User() -> impl IntoView {
|
|||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||
</p>
|
||||
</div>
|
||||
}.into_view()
|
||||
})}
|
||||
})
|
||||
}})}
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue