mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 14:54:16 +00:00
hackernews islands example
This commit is contained in:
parent
ae14644806
commit
5b7f5e3db3
9 changed files with 190 additions and 255 deletions
|
@ -11,16 +11,11 @@ codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
console_log = "1.0"
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
leptos = { path = "../../leptos", features = ["experimental-islands"] }
|
leptos = { path = "../../leptos", features = ["experimental-islands"] }
|
||||||
leptos_axum = { path = "../../integrations/axum", optional = true, features = [
|
leptos_axum = { path = "../../integrations/axum", optional = true }
|
||||||
"experimental-islands",
|
|
||||||
] }
|
|
||||||
leptos_meta = { path = "../../meta" }
|
leptos_meta = { path = "../../meta" }
|
||||||
leptos_router = { path = "../../router" }
|
leptos_router = { path = "../../router" }
|
||||||
log = "0.4"
|
|
||||||
simple_logger = "4.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
gloo-net = { version = "0.6", features = ["http"] }
|
gloo-net = { version = "0.6", features = ["http"] }
|
||||||
|
@ -46,8 +41,8 @@ mime_guess = { version = "2.0.4", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
csr = ["leptos/csr"]
|
||||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
hydrate = ["leptos/hydrate"]
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
#![allow(unused)]
|
#[cfg(feature = "ssr")]
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use leptos::Serializable;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
pub fn story(path: &str) -> String {
|
pub fn story(path: &str) -> String {
|
||||||
format!("https://node-hnapi.herokuapp.com/{path}")
|
format!("https://node-hnapi.herokuapp.com/{path}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
pub fn user(path: &str) -> String {
|
pub fn user(path: &str) -> String {
|
||||||
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
|
format!("https://hacker-news.firebaseio.com/v0/user/{path}.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
|
||||||
static ref CLIENT: reqwest::Client = reqwest::Client::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
pub async fn fetch_api<T>(path: &str) -> Option<T>
|
||||||
where
|
where
|
||||||
T: Serializable,
|
T: Serialize + DeserializeOwned,
|
||||||
{
|
{
|
||||||
let json = CLIENT.get(path).send().await.ok()?.text().await.ok()?;
|
use leptos::logging;
|
||||||
T::de(&json).map_err(|e| log::error!("{e}")).ok()
|
reqwest::get(path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logging::error!("{e}"))
|
||||||
|
.ok()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
use leptos::{view, Errors, For, IntoView, RwSignal, SignalGet, View};
|
|
||||||
|
|
||||||
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
|
|
||||||
// here than just displaying them
|
|
||||||
pub fn error_template(errors: Option<RwSignal<Errors>>) -> View {
|
|
||||||
let Some(errors) = errors else {
|
|
||||||
panic!("No Errors found and we expected errors!");
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<h1>"Errors"</h1>
|
|
||||||
<For
|
|
||||||
// a function that returns the items we're iterating over; a signal is fine
|
|
||||||
each=move || errors.get()
|
|
||||||
// a unique key for each item as a reference
|
|
||||||
key=|(key, _)| key.clone()
|
|
||||||
// renders each item to a view
|
|
||||||
children= move | (_, error)| {
|
|
||||||
let error_string = error.to_string();
|
|
||||||
view! {
|
|
||||||
|
|
||||||
<p>"Error: " {error_string}</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
.into_view()
|
|
||||||
}
|
|
|
@ -1,13 +1,31 @@
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::*;
|
|
||||||
use leptos_router::*;
|
|
||||||
mod api;
|
mod api;
|
||||||
pub mod error_template;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod fallback;
|
|
||||||
mod routes;
|
mod routes;
|
||||||
|
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
|
||||||
|
use leptos_router::{
|
||||||
|
components::{FlatRoutes, Route, Router},
|
||||||
|
ParamSegment, StaticSegment,
|
||||||
|
};
|
||||||
use routes::{nav::*, stories::*, story::*, users::*};
|
use routes::{nav::*, stories::*, story::*, users::*};
|
||||||
|
|
||||||
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<AutoReload options=options.clone() />
|
||||||
|
<HydrationScripts options islands=true/>
|
||||||
|
<MetaTags/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<App/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
provide_meta_context();
|
provide_meta_context();
|
||||||
|
@ -16,66 +34,24 @@ pub fn App() -> impl IntoView {
|
||||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||||
<Example/>
|
|
||||||
<Router>
|
<Router>
|
||||||
<Nav />
|
<Nav />
|
||||||
<main>
|
<main>
|
||||||
<Routes>
|
<FlatRoutes fallback=|| "Not found.">
|
||||||
<Route path="users/:id" view=User ssr=SsrMode::InOrder/>
|
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
|
||||||
<Route path="stories/:id" view=Story ssr=SsrMode::InOrder/>
|
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
|
||||||
<Route path=":stories?" view=Stories ssr=SsrMode::InOrder/>
|
<Route path=ParamSegment("stories") view=Stories/>
|
||||||
</Routes>
|
// TODO allow optional params without duplication
|
||||||
|
<Route path=StaticSegment("") view=Stories/>
|
||||||
|
</FlatRoutes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
#[island]
|
|
||||||
pub fn CommonIsland() -> impl IntoView {
|
|
||||||
let val = RwSignal::new(0);
|
|
||||||
view! {
|
|
||||||
<div>
|
|
||||||
{move || format!("CommonIsland value is {}", val.get())}
|
|
||||||
<button on:click=move|_| val.update(|x| {*x += 1})>Click</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[island]
|
|
||||||
pub fn OuterWorking(children: Children) -> impl IntoView {
|
|
||||||
let val = RwSignal::new(0);
|
|
||||||
view! {
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
{move || format!("outer value is {}", val.get())}
|
|
||||||
<button on:click=move|_| val.update(|x| {*x += 1})>Click</button>
|
|
||||||
</div>
|
|
||||||
{children()}
|
|
||||||
</>
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn Example() -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<OuterFailing/>
|
|
||||||
|
|
||||||
<OuterWorking>
|
|
||||||
<CommonIsland/>
|
|
||||||
</OuterWorking>
|
|
||||||
|
|
||||||
<CommonIsland/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn hydrate() {
|
pub fn hydrate() {
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
leptos::leptos_dom::HydrationCtx::stop_hydrating();
|
leptos::mount::hydrate_islands();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
pub use axum::{routing::get, Router};
|
pub use axum::Router;
|
||||||
pub use hackernews_islands::fallback::file_and_error_handler;
|
|
||||||
use hackernews_islands::*;
|
use hackernews_islands::*;
|
||||||
pub use leptos::get_configuration;
|
pub use leptos::config::get_configuration;
|
||||||
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
pub use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use tower_http::compression::{
|
use tower_http::compression::{
|
||||||
predicate::{NotForContentType, SizeAbove},
|
predicate::{NotForContentType, SizeAbove},
|
||||||
|
@ -50,7 +49,6 @@ async fn main() {
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
use hackernews_islands::*;
|
use hackernews_islands::*;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
_ = console_log::init_with_level(log::Level::Debug);
|
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
mount_to_body(App);
|
leptos::mount::mount_to_body(App);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use leptos::{component, view, IntoView};
|
use leptos::prelude::*;
|
||||||
use leptos_router::*;
|
use leptos_router::components::A;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Nav() -> impl IntoView {
|
pub fn Nav() -> impl IntoView {
|
||||||
|
@ -21,7 +21,7 @@ pub fn Nav() -> impl IntoView {
|
||||||
<A href="/job">
|
<A href="/job">
|
||||||
<strong>"Jobs"</strong>
|
<strong>"Jobs"</strong>
|
||||||
</A>
|
</A>
|
||||||
<a class="github" href="http://github.com/gbj/leptos" target="_blank" rel="noreferrer">
|
<a class="github" href="http://github.com/leptos-rs/leptos" target="_blank" rel="noreferrer">
|
||||||
"Built with Leptos"
|
"Built with Leptos"
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use leptos::prelude::*;
|
use leptos::{either::Either, prelude::*};
|
||||||
use leptos_router::*;
|
use leptos_router::{
|
||||||
|
components::A,
|
||||||
|
hooks::{use_params_map, use_query_map},
|
||||||
|
};
|
||||||
|
|
||||||
fn category(from: &str) -> String {
|
fn category(from: &str) -> String {
|
||||||
match from {
|
match from {
|
||||||
|
@ -13,7 +16,7 @@ fn category(from: &str) -> String {
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(FetchStories, "/api")]
|
#[server]
|
||||||
pub async fn fetch_stories(
|
pub async fn fetch_stories(
|
||||||
story_type: String,
|
story_type: String,
|
||||||
page: usize,
|
page: usize,
|
||||||
|
@ -30,80 +33,84 @@ pub fn Stories() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
let page = move || {
|
let page = move || {
|
||||||
query
|
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)
|
.unwrap_or(1)
|
||||||
};
|
};
|
||||||
let story_type = move || {
|
let story_type = move || {
|
||||||
params
|
params
|
||||||
.with(|p| p.get("stories").cloned())
|
.read()
|
||||||
|
.get("stories")
|
||||||
.unwrap_or_else(|| "top".to_string())
|
.unwrap_or_else(|| "top".to_string())
|
||||||
};
|
};
|
||||||
let stories = create_resource(
|
let stories = Resource::new(
|
||||||
move || (page(), story_type()),
|
move || (page(), story_type()),
|
||||||
move |(page, story_type)| fetch_stories(category(&story_type), page),
|
move |(page, story_type)| async move {
|
||||||
|
fetch_stories(category(&story_type), page).await.ok()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let (pending, set_pending) = create_signal(false);
|
let (pending, set_pending) = signal(false);
|
||||||
|
|
||||||
let hide_more_link = move || {
|
let hide_more_link = move || match &*stories.read() {
|
||||||
pending.get()
|
Some(Some(stories)) => stories.len() < 28,
|
||||||
|| stories
|
_ => true
|
||||||
.map(|stories| {
|
} || pending.get();
|
||||||
stories.as_ref().map(|s| s.len() < 28).unwrap_or_default()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
|
||||||
<div class="news-view">
|
<div class="news-view">
|
||||||
<div class="news-list-nav">
|
<div class="news-list-nav">
|
||||||
<span>
|
<span>
|
||||||
{move || if page() > 1 {
|
{move || if page() > 1 {
|
||||||
view! {
|
Either::Left(view! {
|
||||||
<a class="page-link"
|
<a class="page-link"
|
||||||
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
href=move || format!("/{}?page={}", story_type(), page() - 1)
|
||||||
attr:aria_label="Previous Page"
|
aria-label="Previous Page"
|
||||||
>
|
>
|
||||||
"< prev"
|
"< prev"
|
||||||
</a>
|
</a>
|
||||||
}.into_any()
|
})
|
||||||
} else {
|
} else {
|
||||||
view! {
|
Either::Right(view! {
|
||||||
<span class="page-link disabled" aria-hidden="true">
|
<span class="page-link disabled" aria-hidden="true">
|
||||||
"< prev"
|
"< prev"
|
||||||
</span>
|
</span>
|
||||||
}.into_any()
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span>"page " {page}</span>
|
<span>"page " {page}</span>
|
||||||
<span class="page-link"
|
<Suspense>
|
||||||
class:disabled=hide_more_link
|
<span class="page-link"
|
||||||
aria-hidden=hide_more_link
|
class:disabled=hide_more_link
|
||||||
>
|
aria-hidden=hide_more_link
|
||||||
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
|
||||||
aria-label="Next Page"
|
|
||||||
>
|
>
|
||||||
"more >"
|
<a href=move || format!("/{}?page={}", story_type(), page() + 1)
|
||||||
</a>
|
aria-label="Next Page"
|
||||||
</span>
|
>
|
||||||
|
"more >"
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<main class="news-list">
|
<main class="news-list">
|
||||||
<div>
|
<div>
|
||||||
<Transition
|
<Transition
|
||||||
fallback=|| ()
|
fallback=move || view! { <p>"Loading..."</p> }
|
||||||
set_pending
|
set_pending
|
||||||
>
|
>
|
||||||
{move || stories.get().map(|story| story.map(|stories| view! {
|
<Show when=move || stories.read().as_ref().map(Option::is_none).unwrap_or(false)>
|
||||||
<ul>
|
>
|
||||||
<For
|
<p>"Error loading stories."</p>
|
||||||
each=move || stories.clone()
|
</Show>
|
||||||
key=|story| story.id
|
<ul>
|
||||||
let:story
|
<For
|
||||||
>
|
each=move || stories.get().unwrap_or_default().unwrap_or_default()
|
||||||
<Story story/>
|
key=|story| story.id
|
||||||
</For>
|
let:story
|
||||||
</ul>
|
>
|
||||||
}))}
|
<Story story/>
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -118,26 +125,26 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||||
<span class="score">{story.points}</span>
|
<span class="score">{story.points}</span>
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{if !story.url.starts_with("item?id=") {
|
{if !story.url.starts_with("item?id=") {
|
||||||
view! {
|
Either::Left(view! {
|
||||||
<span>
|
<span>
|
||||||
<a href=story.url target="_blank" rel="noreferrer">
|
<a href=story.url target="_blank" rel="noreferrer">
|
||||||
{story.title.clone()}
|
{story.title.clone()}
|
||||||
</a>
|
</a>
|
||||||
<span class="host">"("{story.domain}")"</span>
|
<span class="host">"("{story.domain}")"</span>
|
||||||
</span>
|
</span>
|
||||||
}.into_view()
|
})
|
||||||
} else {
|
} else {
|
||||||
let title = story.title.clone();
|
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>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span class="meta">
|
<span class="meta">
|
||||||
{if story.story_type != "job" {
|
{if story.story_type != "job" {
|
||||||
view! {
|
Either::Left(view! {
|
||||||
<span>
|
<span>
|
||||||
{"by "}
|
{"by "}
|
||||||
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
{story.user.map(|user| view ! { <A href=format!("/users/{user}")>{user.clone()}</A>})}
|
||||||
{format!(" {} | ", story.time_ago)}
|
{format!(" {} | ", story.time_ago)}
|
||||||
<A href=format!("/stories/{}", story.id)>
|
<A href=format!("/stories/{}", story.id)>
|
||||||
{if story.comments_count.unwrap_or_default() > 0 {
|
{if story.comments_count.unwrap_or_default() > 0 {
|
||||||
|
@ -147,10 +154,10 @@ fn Story(story: api::Story) -> impl IntoView {
|
||||||
}}
|
}}
|
||||||
</A>
|
</A>
|
||||||
</span>
|
</span>
|
||||||
}.into_view()
|
})
|
||||||
} else {
|
} else {
|
||||||
let title = story.title.clone();
|
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>
|
</span>
|
||||||
{(story.story_type != "link").then(|| view! {
|
{(story.story_type != "link").then(|| view! {
|
||||||
|
|
|
@ -1,52 +1,37 @@
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use leptos::either::Either;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::Meta;
|
||||||
use leptos_router::*;
|
use leptos_router::components::A;
|
||||||
use std::cell::RefCell;
|
use leptos_router::hooks::use_params_map;
|
||||||
|
|
||||||
#[server(FetchStory, "/api")]
|
#[server]
|
||||||
pub async fn fetch_story(
|
pub async fn fetch_story(
|
||||||
id: String,
|
id: String,
|
||||||
) -> Result<RefCell<Option<api::Story>>, ServerFnError> {
|
) -> Result<Option<api::Story>, ServerFnError> {
|
||||||
Ok(RefCell::new(
|
Ok(api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await)
|
||||||
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}"))).await,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Story() -> impl IntoView {
|
pub fn Story() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
let story = create_resource(
|
let story = Resource::new(
|
||||||
move || params.get().get("id").cloned().unwrap_or_default(),
|
move || params.read().get("id").unwrap_or_default(),
|
||||||
move |id| async move {
|
move |id| async move {
|
||||||
if id.is_empty() {
|
if id.is_empty() {
|
||||||
Ok(RefCell::new(None))
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
fetch_story(id).await
|
fetch_story(id).await
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let meta_description = move || {
|
|
||||||
story
|
|
||||||
.map(|story| {
|
|
||||||
story
|
|
||||||
.as_ref()
|
|
||||||
.map(|story| {
|
|
||||||
story.borrow().as_ref().map(|story| story.title.clone())
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or_else(|| "Loading story...".to_string())
|
|
||||||
};
|
|
||||||
|
|
||||||
let story_view = move || {
|
Suspense(SuspenseProps::builder().fallback(|| "Loading...").children(ToChildren::to_children(move || Suspend(async move {
|
||||||
story.map(|story| {
|
match story.await.ok().flatten() {
|
||||||
story.as_ref().ok().and_then(|story| {
|
None => Either::Left("Story not found."),
|
||||||
let story: Option<api::Story> = story.borrow_mut().take();
|
Some(story) => {
|
||||||
story.map(|story| {
|
Either::Right(view! {
|
||||||
view! {
|
<Meta name="description" content=story.title.clone()/>
|
||||||
<div class="item-view">
|
<div class="item-view">
|
||||||
<div class="item-view-header">
|
<div class="item-view-header">
|
||||||
<a href=story.url target="_blank">
|
<a href=story.url target="_blank">
|
||||||
|
@ -55,63 +40,36 @@ pub fn Story() -> impl IntoView {
|
||||||
<span class="host">
|
<span class="host">
|
||||||
"("{story.domain}")"
|
"("{story.domain}")"
|
||||||
</span>
|
</span>
|
||||||
{story.user.map(|user| view! { <p class="meta">
|
{story.user.map(|user| view! { <p class="meta">
|
||||||
{story.points}
|
{story.points}
|
||||||
" points | by "
|
" points | by "
|
||||||
<A href=format!("/users/{user}")>{user.clone()}</A>
|
<A href=format!("/users/{user}")>{user.clone()}</A>
|
||||||
{format!(" {}", story.time_ago)}
|
{format!(" {}", story.time_ago)}
|
||||||
</p>})}
|
</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>
|
||||||
|
</div>
|
||||||
</div>
|
</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">
|
|
||||||
{story.comments.unwrap_or_default().into_iter()
|
|
||||||
.map(|comment: api::Comment| view! { <Comment comment /> })
|
|
||||||
.collect_view()}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}})})})
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<Meta name="description" content=meta_description/>
|
|
||||||
<Suspense fallback=|| ()>
|
|
||||||
{story_view}
|
|
||||||
</Suspense>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[island]
|
|
||||||
pub fn Toggle(children: Children) -> impl IntoView {
|
|
||||||
let (open, set_open) = create_signal(true);
|
|
||||||
view! {
|
|
||||||
<div class="toggle" class:open=open>
|
|
||||||
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
|
||||||
{move || if open.get() {
|
|
||||||
"[-]"
|
|
||||||
} else {
|
|
||||||
"[+] comments collapsed"
|
|
||||||
}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
class="comment-children"
|
|
||||||
style:display=move || if open.get() {
|
|
||||||
"block"
|
|
||||||
} else {
|
|
||||||
"none"
|
|
||||||
}
|
}
|
||||||
>
|
}
|
||||||
{children()}
|
}))).build())
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -135,3 +93,29 @@ pub fn Comment(comment: api::Comment) -> impl IntoView {
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[island]
|
||||||
|
pub fn Toggle(children: Children) -> impl IntoView {
|
||||||
|
let (open, set_open) = signal(true);
|
||||||
|
view! {
|
||||||
|
<div class="toggle" class:open=open>
|
||||||
|
<a on:click=move |_| set_open.update(|n| *n = !*n)>
|
||||||
|
{move || if open.get() {
|
||||||
|
"[-]"
|
||||||
|
} else {
|
||||||
|
"[+] comments collapsed"
|
||||||
|
}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
class="comment-children"
|
||||||
|
style:display=move || if open.get() {
|
||||||
|
"block"
|
||||||
|
} else {
|
||||||
|
"none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children()}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
#[allow(unused)] // User is unused in WASM build
|
use crate::api;
|
||||||
use crate::api::{self, User};
|
use leptos::server::Resource;
|
||||||
use leptos::prelude::*;
|
use leptos::{either::Either, prelude::*};
|
||||||
use leptos_router::*;
|
use leptos_router::hooks::use_params_map;
|
||||||
|
|
||||||
#[server(FetchUser, "/api")]
|
#[server]
|
||||||
pub async fn fetch_user(
|
pub async fn fetch_user(
|
||||||
id: String,
|
id: String,
|
||||||
) -> Result<Option<api::User>, ServerFnError> {
|
) -> Result<Option<api::User>, ServerFnError> {
|
||||||
Ok(api::fetch_api::<User>(&api::user(&id)).await)
|
Ok(api::fetch_api::<api::User>(&api::user(&id)).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn User() -> impl IntoView {
|
pub fn User() -> impl IntoView {
|
||||||
let params = use_params_map();
|
let params = use_params_map();
|
||||||
let user = create_resource(
|
let user = Resource::new(
|
||||||
move || params.get().get("id").cloned().unwrap_or_default(),
|
move || params.read().get("id").unwrap_or_default(),
|
||||||
move |id| async move {
|
move |id| async move {
|
||||||
if id.is_empty() {
|
if id.is_empty() {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -25,12 +25,12 @@ pub fn User() -> impl IntoView {
|
||||||
);
|
);
|
||||||
view! {
|
view! {
|
||||||
<div class="user-view">
|
<div class="user-view">
|
||||||
<Suspense fallback=|| ()>
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
{move || user.get().map(|user| user.map(|user| match user {
|
{move || Suspend(async move { match user.await.ok().flatten() {
|
||||||
None => view! { <h1>"User not found."</h1> }.into_view(),
|
None => Either::Left(view! { <h1>"User not found."</h1> }),
|
||||||
Some(user) => view! {
|
Some(user) => Either::Right(view! {
|
||||||
<div>
|
<div>
|
||||||
<h1>"User: " {&user.id}</h1>
|
<h1>"User: " {user.id.clone()}</h1>
|
||||||
<ul class="meta">
|
<ul class="meta">
|
||||||
<li>
|
<li>
|
||||||
<span class="label">"Created: "</span> {user.created}
|
<span class="label">"Created: "</span> {user.created}
|
||||||
|
@ -46,8 +46,8 @@ pub fn User() -> impl IntoView {
|
||||||
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
})
|
||||||
}))}
|
}})}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue