1
0
Fork 0
mirror of https://github.com/Nutomic/ibis.git synced 2024-11-22 18:41:09 +00:00

Merge branch 'dessalines-add_leptosfmt'

This commit is contained in:
Felix Ableitner 2024-10-02 13:16:51 +02:00
commit 706713be9d
25 changed files with 703 additions and 482 deletions

2
.leptosfmt.toml Normal file
View file

@ -0,0 +1,2 @@
max_width = 100
attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve"

View file

@ -1,5 +1,6 @@
variables: variables:
- &rust_image "rust:1.75" - &rust_image "rust:1.75"
- &install_binstall "wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && cp cargo-binstall /usr/local/cargo/bin"
steps: steps:
cargo_fmt: cargo_fmt:
@ -10,6 +11,20 @@ steps:
commands: commands:
- rustup component add rustfmt - rustup component add rustfmt
- cargo +nightly fmt -- --check - cargo +nightly fmt -- --check
leptos_fmt:
image: *rust_image
commands:
- *install_binstall
- cargo binstall -y leptosfmt
- leptosfmt -c .leptosfmt.toml --check src
when:
- event: pull_request
toml_fmt:
image: tamasfe/taplo:0.8.1
commands:
- taplo format --check
when: when:
- event: pull_request - event: pull_request

2
Cargo.lock generated
View file

@ -1587,7 +1587,7 @@ dependencies = [
[[package]] [[package]]
name = "ibis" name = "ibis"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"activitypub_federation", "activitypub_federation",
"anyhow", "anyhow",

View file

@ -45,7 +45,7 @@ diesel = { version = "2.1.4", features = [
"postgres", "postgres",
"chrono", "chrono",
"uuid", "uuid",
"r2d2" "r2d2",
], optional = true } ], optional = true }
diesel-derive-newtype = { version = "2.1.0", optional = true } diesel-derive-newtype = { version = "2.1.0", optional = true }
diesel_migrations = { version = "2.1.0", optional = true } diesel_migrations = { version = "2.1.0", optional = true }
@ -97,5 +97,5 @@ debug = 0
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = true # Automatically strip symbols from the binary. strip = true # Automatically strip symbols from the binary.
#opt-level = "z" # Optimize for size. #opt-level = "z" # Optimize for size.

View file

@ -88,7 +88,7 @@ pub async fn start(config: IbisConfig) -> MyResult<()> {
let config = data.clone(); let config = data.clone();
let app = Router::new() let app = Router::new()
.leptos_routes(&leptos_options, routes, || view! { <App/> }) .leptos_routes(&leptos_options, routes, || view! { <App/> })
.with_state(leptos_options) .with_state(leptos_options)
.nest("", asset_routes()?) .nest("", asset_routes()?)
.nest(FEDERATION_ROUTES_PREFIX, federation_routes()) .nest(FEDERATION_ROUTES_PREFIX, federation_routes())

View file

@ -90,30 +90,30 @@ pub fn App() -> impl IntoView {
provide_context(create_rw_signal(global_state)); provide_context(create_rw_signal(global_state));
view! { view! {
<> <>
<Stylesheet id="simple" href="/assets/simple.css"/> <Stylesheet id="simple" href="/assets/simple.css"/>
<Stylesheet id="ibis" href="/assets/ibis.css"/> <Stylesheet id="ibis" href="/assets/ibis.css"/>
<Router> <Router>
<Nav /> <Nav/>
<main> <main>
<Routes> <Routes>
<Route path="/" view=ReadArticle/> <Route path="/" view=ReadArticle/>
<Route path="/article/:title" view=ReadArticle/> <Route path="/article/:title" view=ReadArticle/>
<Route path="/article/:title/history" view=ArticleHistory/> <Route path="/article/:title/history" view=ArticleHistory/>
<Route path="/article/:title/edit/:conflict_id?" view=EditArticle/> <Route path="/article/:title/edit/:conflict_id?" view=EditArticle/>
<Route path="/article/:title/actions" view=ArticleActions/> <Route path="/article/:title/actions" view=ArticleActions/>
<Route path="/article/:title/diff/:hash" view=EditDiff/> <Route path="/article/:title/diff/:hash" view=EditDiff/>
<Route path="/article/create" view=CreateArticle/> <Route path="/article/create" view=CreateArticle/>
<Route path="/article/list" view=ListArticles/> <Route path="/article/list" view=ListArticles/>
<Route path="/instance/:hostname" view=InstanceDetails/> <Route path="/instance/:hostname" view=InstanceDetails/>
<Route path="/user/:name" view=UserProfile/> <Route path="/user/:name" view=UserProfile/>
<Route path="/login" view=Login/> <Route path="/login" view=Login/>
<Route path="/register" view=Register/> <Route path="/register" view=Register/>
<Route path="/search" view=Search/> <Route path="/search" view=Search/>
<Route path="/conflicts" view=Conflicts/> <Route path="/conflicts" view=Conflicts/>
</Routes> </Routes>
</main> </main>
</Router> </Router>
</> </>
} }
} }

View file

@ -12,42 +12,61 @@ use leptos_router::*;
#[component] #[component]
pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView { pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView {
view! { view! {
<Suspense> <Suspense>
{move || article.get().map(|article_| { {move || {
let instance = create_local_resource(move || article_.article.instance_id, move |instance_id| async move { article
let form = GetInstance { .get()
id: Some(instance_id) .map(|article_| {
}; let instance = create_local_resource(
GlobalState::api_client() move || article_.article.instance_id,
.get_instance(&form) move |instance_id| async move {
.await let form = GetInstance {
.unwrap() id: Some(instance_id),
}); };
let global_state = use_context::<RwSignal<GlobalState>>().unwrap(); GlobalState::api_client().get_instance(&form).await.unwrap()
let article_link = article_link(&article_.article); },
let article_link_ = article_link.clone(); );
let protected = article_.article.protected; let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
view!{ let article_link = article_link(&article_.article);
<nav class="inner"> let article_link_ = article_link.clone();
let protected = article_.article.protected;
view! {
<nav class="inner">
<A href=article_link.clone()>"Read"</A> <A href=article_link.clone()>"Read"</A>
<A href={format!("{article_link}/history")}>"History"</A> <A href=format!("{article_link}/history")>"History"</A>
<Show when=move || global_state.with(|state| { <Show when=move || {
let is_admin = state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or(false); global_state
state.my_profile.is_some() && can_edit_article(&article_.article, is_admin).is_ok() .with(|state| {
})> let is_admin = state
<A href={format!("{article_link}/edit")}>"Edit"</A> .my_profile
.as_ref()
.map(|p| p.local_user.admin)
.unwrap_or(false);
state.my_profile.is_some()
&& can_edit_article(&article_.article, is_admin).is_ok()
})
}>
<A href=format!("{article_link}/edit")>"Edit"</A>
</Show> </Show>
<Show when=move || global_state.with(|state| state.my_profile.is_some())> <Show when=move || global_state.with(|state| state.my_profile.is_some())>
<A href={format!("{article_link_}/actions")}>"Actions"</A> <A href=format!("{article_link_}/actions")>"Actions"</A>
{instance.get().map(|i| {instance
view!{ <InstanceFollowButton instance=i.instance.clone() /> } .get()
)} .map(|i| {
view! { <InstanceFollowButton instance=i.instance.clone()/> }
})}
</Show> </Show>
<Show when=move || protected> <Show when=move || protected>
<span title="Article can only be edited by local admins">"Protected"</span> <span title="Article can only be edited by local admins">
"Protected"
</span>
</Show> </Show>
</nav> </nav>
}})} }
</Suspense> })
}}
</Suspense>
} }
} }

View file

@ -18,57 +18,63 @@ pub fn CredentialsForm(
}); });
view! { view! {
<form on:submit=|ev| ev.prevent_default()> <form on:submit=|ev| ev.prevent_default()>
<p>{title}</p> <p>{title}</p>
{move || { {move || {
error error
.get() .get()
.map(|err| { .map(|err| {
view! { <p style="color:red;">{err}</p> } view! { <p style="color:red;">{err}</p> }
}) })
}} }}
<input
type="text" <input
required type="text"
placeholder="Username" required
prop:disabled=move || disabled.get() placeholder="Username"
on:keyup=move |ev: ev::KeyboardEvent| { prop:disabled=move || disabled.get()
let val = event_target_value(&ev); on:keyup=move |ev: ev::KeyboardEvent| {
set_username.update(|v| *v = val); let val = event_target_value(&ev);
} set_username.update(|v| *v = val);
on:change=move |ev| { }
let val = event_target_value(&ev);
set_username.update(|v| *v = val); on:change=move |ev| {
} let val = event_target_value(&ev);
/> set_username.update(|v| *v = val);
<input }
type="password" />
required
placeholder="Password" <input
prop:disabled=move || disabled.get() type="password"
on:keyup=move |ev: ev::KeyboardEvent| { required
match &*ev.key() { placeholder="Password"
"Enter" => { prop:disabled=move || disabled.get()
dispatch_action(); on:keyup=move |ev: ev::KeyboardEvent| {
} match &*ev.key() {
_ => { "Enter" => {
let val = event_target_value(&ev); dispatch_action();
set_password.update(|p| *p = val); }
} _ => {
} let val = event_target_value(&ev);
} set_password.update(|p| *p = val);
on:change=move |ev| { }
let val = event_target_value(&ev); }
set_password.update(|p| *p = val); }
}
/> on:change=move |ev| {
<div> let val = event_target_value(&ev);
<button set_password.update(|p| *p = val);
prop:disabled=move || button_is_disabled.get() }
on:click=move |_| dispatch_action()> />
{action_label}
</button> <div>
</div> <button
</form> prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action()
>
{action_label}
</button>
</div>
</form>
} }
} }

View file

@ -30,10 +30,12 @@ pub fn InstanceFollowButton(instance: DbInstance) -> impl IntoView {
}; };
view! { view! {
<button on:click=move |_| follow_action.dispatch(instance.id) <button
prop:disabled=move || is_following on:click=move |_| follow_action.dispatch(instance.id)
prop:hidden=move || instance.local> prop:disabled=move || is_following
{follow_text} prop:hidden=move || instance.local
</button> >
{follow_text}
</button>
} }
} }

View file

@ -22,70 +22,77 @@ pub fn Nav() -> impl IntoView {
let (search_query, set_search_query) = create_signal(String::new()); let (search_query, set_search_query) = create_signal(String::new());
view! { view! {
<nav class="inner" style="min-width: 250px;"> <nav class="inner" style="min-width: 250px;">
<li> <li>
<A href="/">"Main Page"</A> <A href="/">"Main Page"</A>
</li> </li>
<li> <li>
<A href="/article/list">"List Articles"</A> <A href="/article/list">"List Articles"</A>
</li> </li>
<Show <Show when=move || global_state.with(|state| state.my_profile.is_some())>
when=move || global_state.with(|state| state.my_profile.is_some())> <li>
<A href="/article/create">"Create Article"</A>
</li>
<li>
<A href="/conflicts">"Edit Conflicts"</A>
</li>
</Show>
<li>
<form on:submit=move |ev| {
ev.prevent_default();
let navigate = leptos_router::use_navigate();
let query = search_query.get();
if !query.is_empty() {
navigate(&format!("/search?query={query}"), Default::default());
}
}>
<input
type="text"
placeholder="Search"
prop:value=search_query
on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev);
set_search_query.update(|v| *v = val);
}
/>
<button>Go</button>
</form>
</li>
<Show
when=move || global_state.with(|state| state.my_profile.is_some())
fallback=move || {
view! {
<li> <li>
<A href="/article/create">"Create Article"</A> <A href="/login">"Login"</A>
</li> </li>
<li> <Show when=move || registration_open.get().unwrap_or_default()>
<A href="/conflicts">"Edit Conflicts"</A> <li>
</li> <A href="/register">"Register"</A>
</Show> </li>
<li> </Show>
<form on:submit=move |ev| { }
ev.prevent_default(); }
let navigate = leptos_router::use_navigate(); >
let query = search_query.get();
if !query.is_empty() { {
navigate(&format!("/search?query={query}"), Default::default()); let my_profile = global_state.with(|state| state.my_profile.clone().unwrap());
} let profile_link = format!("/user/{}", my_profile.person.username);
}> view! {
<input type="text" placeholder="Search" <p>
prop:value=search_query "Logged in as "
on:keyup=move |ev: ev::KeyboardEvent| { <a
let val = event_target_value(&ev); href=profile_link
set_search_query.update(|v| *v = val); style="border: none; padding: 0; color: var(--accent) !important;"
} /> >
<button>Go</button> {my_profile.person.username}
</form> </a>
</li> </p>
<Show <button on:click=move |_| logout_action.dispatch(())>Logout</button>
when=move || global_state.with(|state| state.my_profile.is_some()) }
fallback=move || { }
view! {
<li> </Show>
<A href="/login">"Login"</A> </nav>
</li>
<Show when=move || registration_open.get().unwrap_or_default()>
<li>
<A href="/register">"Register"</A>
</li>
</Show>
}
}
>
{
let my_profile = global_state.with(|state| state.my_profile.clone().unwrap());
let profile_link = format!("/user/{}", my_profile.person.username);
view ! {
<p>"Logged in as "
<a href=profile_link style="border: none; padding: 0; color: var(--accent) !important;">
{my_profile.person.username}
</a>
</p>
<button on:click=move |_| logout_action.dispatch(())>
Logout
</button>
}
}
</Show>
</nav>
} }
} }

View file

@ -43,7 +43,5 @@ fn user_title(person: &DbPerson) -> String {
fn user_link(person: &DbPerson) -> impl IntoView { fn user_link(person: &DbPerson) -> impl IntoView {
let creator_path = format!("/user/{}", person.username); let creator_path = format!("/user/{}", person.username);
view! { view! { <a href=creator_path>{user_title(person)}</a> }
<a href={creator_path}>{user_title(person)}</a>
}
} }

View file

@ -52,50 +52,73 @@ pub fn ArticleActions() -> impl IntoView {
} }
}); });
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || article.get().map(|article| view! { "Loading..." }
view! { }>
<div class="item-view"> {move || {
<h1>{article_title(&article.article)}</h1> article
{move || { .get()
error .map(|article| {
.get() view! {
.map(|err| { <div class="item-view">
view! { <p style="color:red;">{err}</p> } <h1>{article_title(&article.article)}</h1>
}) {move || {
}} error
<Show .get()
when=move || global_state.with(|state| { .map(|err| {
state.my_profile.as_ref().map(|p| p.local_user.admin).unwrap_or_default() view! { <p style="color:red;">{err}</p> }
&& article.article.local })
})> }}
<button
on:click=move |_| protect_action.dispatch((article.article.id, article.article.protected))>Toggle Article Protection</button> <Show when=move || {
<p>"Protect a local article so that only admins can edit it"</p> global_state
</Show> .with(|state| {
<Show when=move || !article.article.local> state
<input .my_profile
.as_ref()
.map(|p| p.local_user.admin)
.unwrap_or_default() && article.article.local
})
}>
<button on:click=move |_| {
protect_action
.dispatch((article.article.id, article.article.protected))
}>Toggle Article Protection</button>
<p>"Protect a local article so that only admins can edit it"</p>
</Show>
<Show when=move || !article.article.local>
<input
placeholder="New Title" placeholder="New Title"
on:keyup=move |ev: ev::KeyboardEvent| { on:keyup=move |ev: ev::KeyboardEvent| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_new_title.update(|v| *v = val); set_new_title.update(|v| *v = val);
} /> }
<button />
<button
disabled=move || new_title.get().is_empty() disabled=move || new_title.get().is_empty()
on:click=move |_| fork_action.dispatch((article.article.id, new_title.get()))>Fork Article</button> on:click=move |_| {
<p> fork_action.dispatch((article.article.id, new_title.get()))
}
>
Fork Article
</button>
<p>
"You can fork a remote article to the local instance. This is useful if the original "You can fork a remote article to the local instance. This is useful if the original
instance is dead, or if there are disagreements how the article should be written." instance is dead, or if there are disagreements how the article should be written."
</p> </p>
</Show> </Show>
</div> </div>
}) }
} })
</Suspense> }}
<Show when=move || fork_response.get().is_some()>
<Redirect path={article_link(&fork_response.get().unwrap())}/> </Suspense>
</Show> <Show when=move || fork_response.get().is_some()>
<p>"TODO: add option for admin to delete article etc"</p> <Redirect path=article_link(&fork_response.get().unwrap())/>
</Show>
<p>"TODO: add option for admin to delete article etc"</p>
} }
} }

View file

@ -40,50 +40,66 @@ pub fn CreateArticle() -> impl IntoView {
}); });
view! { view! {
<h1>Create new Article</h1> <h1>Create new Article</h1>
<Show <Show
when=move || create_response.get().is_some() when=move || create_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<div class="item-view"> <div class="item-view">
<input <input
type="text" type="text"
required required
placeholder="Title" placeholder="Title"
prop:disabled=move || wait_for_response.get() prop:disabled=move || wait_for_response.get()
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_title.update(|v| *v = val); set_title.update(|v| *v = val);
} }
/> />
<textarea placeholder="Article text..." on:keyup=move |ev| {
let val = event_target_value(&ev); <textarea
set_text.update(|p| *p = val); placeholder="Article text..."
} > on:keyup=move |ev| {
</textarea> let val = event_target_value(&ev);
<div><a href="https://commonmark.org/help/" target="blank_">Markdown</a>" formatting is supported"</div> set_text.update(|p| *p = val);
{move || { }
create_error >
.get() </textarea>
.map(|err| { <div>
view! { <p style="color:red;">{err}</p> } <a href="https://commonmark.org/help/" target="blank_">
}) Markdown
}} </a>
<input type="text" " formatting is supported"
placeholder="Edit summary" </div>
on:keyup=move |ev| { {move || {
let val = event_target_value(&ev); create_error
set_summary.update(|p| *p = val); .get()
}/> .map(|err| {
<button view! { <p style="color:red;">{err}</p> }
prop:disabled=move || button_is_disabled.get() })
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get()))> }}
Submit
</button> <input
</div> type="text"
} placeholder="Edit summary"
}> on:keyup=move |ev| {
<Redirect path={format!("/article/{}", title.get().replace(' ', "_"))} /> let val = event_target_value(&ev);
</Show> set_summary.update(|p| *p = val);
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get()))
>
Submit
</button>
</div>
}
}
>
<Redirect path=format!("/article/{}", title.get().replace(' ', "_"))/>
</Show>
} }
} }

View file

@ -100,56 +100,82 @@ pub fn EditArticle() -> impl IntoView {
); );
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>
<Show <Show
when=move || edit_response.get() == EditResponse::Success when=move || edit_response.get() == EditResponse::Success
fallback=move || { fallback=move || {
view! { view! {
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || article.get().map(|mut article| { view! { "Loading..." }
}>
{move || {
article
.get()
.map(|mut article| {
if let EditResponse::Conflict(conflict) = edit_response.get() { if let EditResponse::Conflict(conflict) = edit_response.get() {
article.article.text = conflict.three_way_merge; article.article.text = conflict.three_way_merge;
set_summary.set(conflict.summary); set_summary.set(conflict.summary);
} }
// set initial text, otherwise submit with no changes results in empty text
set_text.set(article.article.text.clone()); set_text.set(article.article.text.clone());
let article_ = article.clone(); let article_ = article.clone();
view! { view! {
<div class="item-view"> // set initial text, otherwise submit with no changes results in empty text
<h1>{article_title(&article.article)}</h1> <div class="item-view">
{move || { <h1>{article_title(&article.article)}</h1>
edit_error {move || {
.get() edit_error
.map(|err| { .get()
view! { <p style="color:red;">{err}</p> } .map(|err| {
}) view! { <p style="color:red;">{err}</p> }
}} })
<textarea on:keyup=move |ev| { }}
let val = event_target_value(&ev);
set_text.update(|p| *p = val); <textarea on:keyup=move |ev| {
}> let val = event_target_value(&ev);
{article.article.text.clone()} set_text.update(|p| *p = val);
</textarea> }>{article.article.text.clone()}</textarea>
<div><a href="https://commonmark.org/help/" target="blank_">Markdown</a>" formatting is supported"</div> <div>
<input type="text" <a href="https://commonmark.org/help/" target="blank_">
placeholder="Edit summary" Markdown
value={summary.get_untracked()} </a>
on:keyup=move |ev| { " formatting is supported"
let val = event_target_value(&ev);
set_summary.update(|p| *p = val);
}/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((text.get(), summary.get(), article_.clone(), edit_response.get()))>
Submit
</button>
</div> </div>
<input
type="text"
placeholder="Edit summary"
value=summary.get_untracked()
on:keyup=move |ev| {
let val = event_target_value(&ev);
set_summary.update(|p| *p = val);
}
/>
<button
prop:disabled=move || button_is_disabled.get()
on:click=move |_| {
submit_action
.dispatch((
text.get(),
summary.get(),
article_.clone(),
edit_response.get(),
))
}
>
Submit
</button>
</div>
} }
}) })
} }}
</Suspense>
}}> </Suspense>
Edit successful! }
</Show> }
>
Edit successful!
</Show>
} }
} }

View file

@ -12,23 +12,49 @@ pub fn ArticleHistory() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || article.get().map(|article| { view! { "Loading..." }
view! { }>
<div class="item-view"> {move || {
article
.get()
.map(|article| {
view! {
<div class="item-view">
<h1>{article_title(&article.article)}</h1> <h1>{article_title(&article.article)}</h1>
{
article.edits.into_iter().rev().map(|edit| { {article
let path = format!("/article/{}@{}/diff/{}", article.article.title, extract_domain(&article.article.ap_id), edit.edit.hash.0); .edits
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822()); .into_iter()
view! {<li><a href={path}>{label}</a>" by "{user_link(&edit.creator)}</li> } .rev()
}).collect::<Vec<_>>() .map(|edit| {
} let path = format!(
</div> "/article/{}@{}/diff/{}",
} article.article.title,
}) extract_domain(&article.article.ap_id),
} edit.edit.hash.0,
</Suspense> );
let label = format!(
"{} ({})",
edit.edit.summary,
edit.edit.created.to_rfc2822(),
);
view! {
<li>
<a href=path>{label}</a>
" by "
{user_link(&edit.creator)}
</li>
}
})
.collect::<Vec<_>>()}
</div>
}
})
}}
</Suspense>
} }
} }

View file

@ -21,28 +21,36 @@ pub fn ListArticles() -> impl IntoView {
); );
view! { view! {
<h1>Most recently edited Articles</h1> <h1>Most recently edited Articles</h1>
<Suspense fallback=|| view! { "Loading..." }> <Suspense fallback=|| view! { "Loading..." }>
<fieldset on:input=move |ev| { <fieldset on:input=move |ev| {
let val = ev let val = ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id();
.target() let is_local_only = val == "only-local";
.unwrap() set_only_local.update(|p| *p = is_local_only);
.unchecked_into::<web_sys::HtmlInputElement>() }>
.id(); <input type="radio" name="listing-type" id="only-local"/>
let is_local_only = val == "only-local"; <label for="only-local">Only Local</label>
set_only_local.update(|p| *p = is_local_only); <input type="radio" name="listing-type" id="all" checked/>
}> <label for="all">All</label>
<input type="radio" name="listing-type" id="only-local" /> </fieldset>
<label for="only-local">Only Local</label> <ul>
<input type="radio" name="listing-type" id="all" checked/> {move || {
<label for="all">All</label> articles
</fieldset> .get()
<ul> { .map(|a| {
move || articles.get().map(|a| a.into_iter()
a.into_iter().map(|a| view! { .map(|a| {
<li><a href=article_link(&a)>{article_title(&a)}</a></li> view! {
}).collect::<Vec<_>>()) <li>
} </ul> <a href=article_link(&a)>{article_title(&a)}</a>
</Suspense> </li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
} }
} }

View file

@ -11,17 +11,27 @@ pub fn ReadArticle() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
view! { "Loading..." }
}>
{
let parser = markdown_parser(); let parser = markdown_parser();
move || article.get().map(|article| move || {
view! { article
<div class="item-view"> .get()
<h1>{article_title(&article.article)}</h1> .map(|article| {
<div inner_html={parser.parse(&article.article.text).render()}/> view! {
</div> <div class="item-view">
}) <h1>{article_title(&article.article)}</h1>
<div inner_html=parser.parse(&article.article.text).render()></div>
</div>
}
})
}
} }
</Suspense>
</Suspense>
} }
} }

View file

@ -9,16 +9,27 @@ pub fn Conflicts() -> impl IntoView {
); );
view! { view! {
<h1>Your unresolved edit conflicts</h1> <h1>Your unresolved edit conflicts</h1>
<Suspense fallback=|| view! { "Loading..." }> <Suspense fallback=|| view! { "Loading..." }>
<ul> { <ul>
move || conflicts.get().map(|c| {move || {
c.into_iter().map(|c| { conflicts
let link = format!("{}/edit/{}", article_link(&c.article), c.id); .get()
view! { .map(|c| {
<li><a href=link>{article_title(&c.article)}" - "{c.summary}</a></li> c.into_iter()
}}).collect::<Vec<_>>()) .map(|c| {
} </ul> let link = format!("{}/edit/{}", article_link(&c.article), c.id);
</Suspense> view! {
<li>
<a href=link>{article_title(&c.article)} " - " {c.summary}</a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul>
</Suspense>
} }
} }

View file

@ -8,26 +8,36 @@ pub fn EditDiff() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article/>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || article.get().map(|article| { view! { "Loading..." }
let hash = params }>
.get_untracked() {move || {
.get("hash") article
.cloned().unwrap(); .get()
let edit = article.edits.iter().find(|e| e.edit.hash.0.to_string() == hash).unwrap(); .map(|article| {
let label = format!("{} ({})", edit.edit.summary, edit.edit.created.to_rfc2822()); let hash = params.get_untracked().get("hash").cloned().unwrap();
let edit = article
view! { .edits
<div class="item-view"> .iter()
.find(|e| e.edit.hash.0.to_string() == hash)
.unwrap();
let label = format!(
"{} ({})",
edit.edit.summary,
edit.edit.created.to_rfc2822(),
);
view! {
<div class="item-view">
<h1>{article.article.title.replace('_', " ")}</h1> <h1>{article.article.title.replace('_', " ")}</h1>
<h2>{label}</h2> <h2>{label}</h2>
<p>"by "{user_link(&edit.creator)}</p> <p>"by " {user_link(&edit.creator)}</p>
<pre>{edit.edit.diff.clone()}</pre> <pre>{edit.edit.diff.clone()}</pre>
</div> </div>
} }
}) })
} }}
</Suspense>
</Suspense>
} }
} }

View file

@ -20,22 +20,32 @@ pub fn InstanceDetails() -> impl IntoView {
}); });
view! { view! {
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || instance_profile.get().map(|instance: DbInstance| { view! { "Loading..." }
let instance_ = instance.clone(); }>
view! { {move || {
<h1>{instance.domain}</h1> instance_profile
.get()
.map(|instance: DbInstance| {
let instance_ = instance.clone();
view! {
<h1>{instance.domain}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())> <Show when=move || global_state.with(|state| state.my_profile.is_some())>
<InstanceFollowButton instance=instance_.clone() /> <InstanceFollowButton instance=instance_.clone()/>
</Show> </Show>
<p>Follow the instance so that new edits are federated to your instance.</p> <p>Follow the instance so that new edits are federated to your instance.</p>
<p>"TODO: show a list of articles from the instance. For now you can use the "<a href="/article/list">Article list</a>.</p> <p>
<hr/> "TODO: show a list of articles from the instance. For now you can use the "
<h2>"Description:"</h2> <a href="/article/list">Article list</a> .
<div>{instance.description}</div> </p>
} <hr/>
}) <h2>"Description:"</h2>
}</Suspense> <div>{instance.description}</div>
}
})
}}
</Suspense>
} }
} }

View file

@ -38,21 +38,22 @@ pub fn Login() -> impl IntoView {
let disabled = Signal::derive(move || wait_for_response.get()); let disabled = Signal::derive(move || wait_for_response.get());
view! { view! {
<Show <Show
when=move || login_response.get().is_some() when=move || login_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<CredentialsForm <CredentialsForm
title="Please enter the desired credentials" title="Please enter the desired credentials"
action_label="Login" action_label="Login"
action=login_action action=login_action
error=login_error.into() error=login_error.into()
disabled disabled
/> />
}
} }
> }
<Redirect path="/"/> >
</Show>
<Redirect path="/"/>
</Show>
} }
} }

View file

@ -39,21 +39,22 @@ pub fn Register() -> impl IntoView {
let disabled = Signal::derive(move || wait_for_response.get()); let disabled = Signal::derive(move || wait_for_response.get());
view! { view! {
<Show <Show
when=move || register_response.get().is_some() when=move || register_response.get().is_some()
fallback=move || { fallback=move || {
view! { view! {
<CredentialsForm <CredentialsForm
title="Please enter the desired credentials" title="Please enter the desired credentials"
action_label="Register" action_label="Register"
action=register_action action=register_action
error=register_error.into() error=register_error.into()
disabled disabled
/> />
}
} }
> }
<p>"You have successfully registered."</p> >
</Show>
<p>"You have successfully registered."</p>
</Show>
} }
} }

View file

@ -52,45 +52,67 @@ pub fn Search() -> impl IntoView {
}); });
view! { view! {
<h1>"Search results for "{query}</h1> <h1>"Search results for " {query}</h1>
<Suspense fallback=|| view! { "Loading..." }> { <Suspense fallback=|| {
move || search_results.get().map(move |search_results| { view! { "Loading..." }
let is_empty = search_results.is_empty(); }>
view! { {move || {
<Show when=move || !is_empty search_results
fallback=move || { .get()
let error_view = move || { .map(move |search_results| {
error.get().map(|err| { let is_empty = search_results.is_empty();
view! { <p style="color:red;">{err}</p> }
})
};
view! { view! {
{error_view} <Show
<p>No results found</p> when=move || !is_empty
}}> fallback=move || {
<ul> let error_view = move || {
{ error
// render resolved instance .get()
if let Some(instance) = &search_results.instance { .map(|err| {
let domain = &instance.domain; view! { <p style="color:red;">{err}</p> }
vec![view! { <li> })
<a href={format!("/instance/{domain}")}>{domain}</a> };
</li>}] view! {
} else { vec![] } {error_view}
<p>No results found</p>
}
} }
{ >
// render articles from resolve/search
search_results.articles <ul>
.iter()
.map(|a| view! { <li> // render resolved instance
<a href={article_link(a)}>{article_title(a)}</a> {if let Some(instance) = &search_results.instance {
</li>}) let domain = &instance.domain;
.collect::<Vec<_>>() vec![
} view! {
</ul> <li>
</Show> <a href=format!("/instance/{domain}")>{domain}</a>
}}) </li>
} },
</Suspense> ]
} else {
vec![]
}}
// render articles from resolve/search
{search_results
.articles
.iter()
.map(|a| {
view! {
<li>
<a href=article_link(a)>{article_title(a)}</a>
</li>
}
})
.collect::<Vec<_>>()}
</ul>
</Show>
}
})
}}
</Suspense>
} }
} }

View file

@ -22,20 +22,28 @@ pub fn UserProfile() -> impl IntoView {
}); });
view! { view! {
{move || {
error
.get()
.map(|err| {
view! { <p style="color:red;">{err}</p> }
})
}}
<Suspense fallback=|| {
view! { "Loading..." }
}>
{move || { {move || {
error user_profile
.get() .get()
.map(|err| { .map(|person: DbPerson| {
view! { <p style="color:red;">{err}</p> } view! {
<h1>{user_title(&person)}</h1>
<p>TODO: create actual user profile</p>
}
}) })
}} }}
<Suspense fallback=|| view! { "Loading..." }> {
move || user_profile.get().map(|person: DbPerson| { </Suspense>
view! {
<h1>{user_title(&person)}</h1>
<p>TODO: create actual user profile</p>
}
})
}</Suspense>
} }
} }

View file

@ -28,6 +28,6 @@ fn main() {
_ = console_log::init_with_level(log::Level::Debug); _ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once(); console_error_panic_hook::set_once();
mount_to_body(|| { mount_to_body(|| {
view! { <App/> } view! { <App/> }
}); });
} }