This commit is contained in:
Felix Ableitner 2024-10-02 14:35:32 +02:00
parent bb892571cb
commit 3e1d2a1d4a
19 changed files with 689 additions and 673 deletions

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,61 +12,65 @@ 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 || { {move || {
article article
.get() .get()
.map(|article_| { .map(|article_| {
let instance = create_local_resource( let instance = create_local_resource(
move || article_.article.instance_id, move || article_.article.instance_id,
move |instance_id| async move { move |instance_id| async move {
let form = GetInstance { let form = GetInstance {
id: Some(instance_id), id: Some(instance_id),
}; };
GlobalState::api_client().get_instance(&form).await.unwrap() GlobalState::api_client().get_instance(&form).await.unwrap()
}, },
); );
let global_state = use_context::<RwSignal<GlobalState>>().unwrap(); let global_state = use_context::<RwSignal<GlobalState>>().unwrap();
let article_link = article_link(&article_.article); let article_link = article_link(&article_.article);
let article_link_ = article_link.clone(); let article_link_ = article_link.clone();
let protected = article_.article.protected; let protected = article_.article.protected;
view! { view! {
<nav class="inner"> <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 || { <Show when=move || {
global_state global_state
.with(|state| { .with(|state| {
let is_admin = state let is_admin = state
.my_profile .my_profile
.as_ref() .as_ref()
.map(|p| p.local_user.admin) .map(|p| p.local_user.admin)
.unwrap_or(false); .unwrap_or(false);
state.my_profile.is_some() state.my_profile.is_some()
&& can_edit_article(&article_.article, is_admin).is_ok() && can_edit_article(&article_.article, is_admin).is_ok()
}) })
}> }>
<A href=format!("{article_link}/edit")>"Edit"</A> <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 || {
<A href=format!("{article_link_}/actions")>"Actions"</A> global_state.with(|state| state.my_profile.is_some())
{instance }>
.get() <A href=format!("{article_link_}/actions")>"Actions"</A>
.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"> <span title="Article can only be edited by local admins">
"Protected" "Protected"
</span> </span>
</Show> </Show>
</nav> </nav>
} }
}) })
}} }}
</Suspense> </Suspense>
} }
} }

View File

@ -18,63 +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 <input
type="text" type="text"
required required
placeholder="Username" placeholder="Username"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
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_username.update(|v| *v = val); set_username.update(|v| *v = val);
} }
on:change=move |ev| { on:change=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_username.update(|v| *v = val); set_username.update(|v| *v = val);
} }
/> />
<input <input
type="password" type="password"
required required
placeholder="Password" placeholder="Password"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
on:keyup=move |ev: ev::KeyboardEvent| { on:keyup=move |ev: ev::KeyboardEvent| {
match &*ev.key() { match &*ev.key() {
"Enter" => { "Enter" => {
dispatch_action(); dispatch_action();
} }
_ => { _ => {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_password.update(|p| *p = val); set_password.update(|p| *p = val);
} }
} }
} }
on:change=move |ev| { on:change=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_password.update(|p| *p = val); set_password.update(|p| *p = val);
} }
/> />
<div> <div>
<button <button
prop:disabled=move || button_is_disabled.get() prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action() on:click=move |_| dispatch_action()
> >
{action_label} {action_label}
</button> </button>
</div> </div>
</form> </form>
} }
} }

View File

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

View File

@ -22,77 +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 when=move || global_state.with(|state| state.my_profile.is_some())> <Show 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="/login">"Login"</A> <A href="/article/create">"Create Article"</A>
</li> </li>
<Show when=move || registration_open.get().unwrap_or_default()> <li>
<li> <A href="/conflicts">"Edit Conflicts"</A>
<A href="/register">"Register"</A> </li>
</li> </Show>
</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>
let my_profile = global_state.with(|state| state.my_profile.clone().unwrap()); </form>
let profile_link = format!("/user/{}", my_profile.person.username); </li>
view! { <Show
<p> when=move || global_state.with(|state| state.my_profile.is_some())
"Logged in as " fallback=move || {
<a view! {
href=profile_link <li>
style="border: none; padding: 0; color: var(--accent) !important;" <A href="/login">"Login"</A>
> </li>
{my_profile.person.username} <Show when=move || registration_open.get().unwrap_or_default()>
</a> <li>
</p> <A href="/register">"Register"</A>
<button on:click=move |_| logout_action.dispatch(())>Logout</button> </li>
} </Show>
} }
}
>
</Show> {
</nav> 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

@ -52,73 +52,73 @@ pub fn ArticleActions() -> impl IntoView {
} }
}); });
view! { view! {
<ArticleNav article=article/> <ArticleNav article=article />
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
{move || { {move || {
article article
.get() .get()
.map(|article| { .map(|article| {
view! { view! {
<div class="item-view"> <div class="item-view">
<h1>{article_title(&article.article)}</h1> <h1>{article_title(&article.article)}</h1>
{move || { {move || {
error error
.get() .get()
.map(|err| { .map(|err| {
view! { <p style="color:red;">{err}</p> } view! { <p style="color:red;">{err}</p> }
}) })
}} }}
<Show when=move || { <Show when=move || {
global_state global_state
.with(|state| { .with(|state| {
state state
.my_profile .my_profile
.as_ref() .as_ref()
.map(|p| p.local_user.admin) .map(|p| p.local_user.admin)
.unwrap_or_default() && article.article.local .unwrap_or_default() && article.article.local
}) })
}> }>
<button on:click=move |_| { <button on:click=move |_| {
protect_action protect_action
.dispatch((article.article.id, article.article.protected)) .dispatch((article.article.id, article.article.protected))
}>Toggle Article Protection</button> }>Toggle Article Protection</button>
<p>"Protect a local article so that only admins can edit it"</p> <p>"Protect a local article so that only admins can edit it"</p>
</Show> </Show>
<Show when=move || !article.article.local> <Show when=move || !article.article.local>
<input <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 |_| { on:click=move |_| {
fork_action.dispatch((article.article.id, new_title.get())) fork_action.dispatch((article.article.id, new_title.get()))
} }
> >
Fork Article Fork Article
</button> </button>
<p> <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> </Suspense>
<Show when=move || fork_response.get().is_some()> <Show when=move || fork_response.get().is_some()>
<Redirect path=article_link(&fork_response.get().unwrap())/> <Redirect path=article_link(&fork_response.get().unwrap()) />
</Show> </Show>
<p>"TODO: add option for admin to delete article etc"</p> <p>"TODO: add option for admin to delete article etc"</p>
} }
} }

View File

@ -40,66 +40,67 @@ 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 <textarea
placeholder="Article text..." placeholder="Article text..."
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_text.update(|p| *p = val); set_text.update(|p| *p = val);
} }
> ></textarea>
</textarea> <div>
<div> <a href="https://commonmark.org/help/" target="blank_">
<a href="https://commonmark.org/help/" target="blank_"> Markdown
Markdown </a>
</a> " formatting is supported"
" formatting is supported" </div>
</div> {move || {
{move || { create_error
create_error .get()
.get() .map(|err| {
.map(|err| { view! { <p style="color:red;">{err}</p> }
view! { <p style="color:red;">{err}</p> } })
}) }}
}}
<input <input
type="text" type="text"
placeholder="Edit summary" placeholder="Edit summary"
on:keyup=move |ev| { on:keyup=move |ev| {
let val = event_target_value(&ev); let val = event_target_value(&ev);
set_summary.update(|p| *p = val); set_summary.update(|p| *p = val);
} }
/> />
<button <button
prop:disabled=move || button_is_disabled.get() prop:disabled=move || button_is_disabled.get()
on:click=move |_| submit_action.dispatch((title.get(), text.get(), summary.get())) on:click=move |_| {
> submit_action.dispatch((title.get(), text.get(), summary.get()))
Submit }
</button> >
</div> Submit
</button>
</div>
}
} }
} >
>
<Redirect path=format!("/article/{}", title.get().replace(' ', "_"))/> <Redirect path=format!("/article/{}", title.get().replace(' ', "_")) />
</Show> </Show>
} }
} }

View File

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

View File

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

View File

@ -21,36 +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.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id(); let val = ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().id();
let is_local_only = val == "only-local"; let is_local_only = val == "only-local";
set_only_local.update(|p| *p = is_local_only); set_only_local.update(|p| *p = is_local_only);
}> }>
<input type="radio" name="listing-type" id="only-local"/> <input type="radio" name="listing-type" id="only-local" />
<label for="only-local">Only Local</label> <label for="only-local">Only Local</label>
<input type="radio" name="listing-type" id="all" checked/> <input type="radio" name="listing-type" id="all" checked />
<label for="all">All</label> <label for="all">All</label>
</fieldset> </fieldset>
<ul> <ul>
{move || { {move || {
articles articles
.get() .get()
.map(|a| { .map(|a| {
a.into_iter() a.into_iter()
.map(|a| { .map(|a| {
view! { view! {
<li> <li>
<a href=article_link(&a)>{article_title(&a)}</a> <a href=article_link(&a)>{article_title(&a)}</a>
</li> </li>
} }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
}} }}
</ul> </ul>
</Suspense> </Suspense>
} }
} }

View File

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

View File

@ -9,27 +9,33 @@ 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 || { {move || {
conflicts conflicts
.get() .get()
.map(|c| { .map(|c| {
c.into_iter() c.into_iter()
.map(|c| { .map(|c| {
let link = format!("{}/edit/{}", article_link(&c.article), c.id); let link = format!(
view! { "{}/edit/{}",
<li> article_link(&c.article),
<a href=link>{article_title(&c.article)} " - " {c.summary}</a> c.id,
</li> );
} view! {
}) <li>
.collect::<Vec<_>>() <a href=link>
}) {article_title(&c.article)} " - " {c.summary}
}} </a>
</li>
}
})
.collect::<Vec<_>>()
})
}}
</ul> </ul>
</Suspense> </Suspense>
} }
} }

View File

@ -8,36 +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=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
{move || { {move || {
article article
.get() .get()
.map(|article| { .map(|article| {
let hash = params.get_untracked().get("hash").cloned().unwrap(); let hash = params.get_untracked().get("hash").cloned().unwrap();
let edit = article let edit = article
.edits .edits
.iter() .iter()
.find(|e| e.edit.hash.0.to_string() == hash) .find(|e| e.edit.hash.0.to_string() == hash)
.unwrap(); .unwrap();
let label = format!( let label = format!(
"{} ({})", "{} ({})",
edit.edit.summary, edit.edit.summary,
edit.edit.created.to_rfc2822(), edit.edit.created.to_rfc2822(),
); );
view! { view! {
<div class="item-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,32 +20,36 @@ pub fn InstanceDetails() -> impl IntoView {
}); });
view! { view! {
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
{move || { {move || {
instance_profile instance_profile
.get() .get()
.map(|instance: DbInstance| { .map(|instance: DbInstance| {
let instance_ = instance.clone(); let instance_ = instance.clone();
view! { view! {
<h1>{instance.domain}</h1> <h1>{instance.domain}</h1>
<Show when=move || global_state.with(|state| state.my_profile.is_some())> <Show when=move || {
<InstanceFollowButton instance=instance_.clone()/> global_state.with(|state| state.my_profile.is_some())
</Show> }>
<p>Follow the instance so that new edits are federated to your instance.</p> <InstanceFollowButton instance=instance_.clone() />
<p> </Show>
"TODO: show a list of articles from the instance. For now you can use the " <p>
<a href="/article/list">Article list</a> . Follow the instance so that new edits are federated to your instance.
</p> </p>
<hr/> <p>
<h2>"Description:"</h2> "TODO: show a list of articles from the instance. For now you can use the "
<div>{instance.description}</div> <a href="/article/list">Article list</a>.
} </p>
}) <hr />
}} <h2>"Description:"</h2>
<div>{instance.description}</div>
}
})
}}
</Suspense> </Suspense>
} }
} }

View File

@ -38,22 +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="/"/> <Redirect path="/" />
</Show> </Show>
} }
} }

View File

@ -39,22 +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> <p>"You have successfully registered."</p>
</Show> </Show>
} }
} }

View File

@ -52,67 +52,66 @@ pub fn Search() -> impl IntoView {
}); });
view! { view! {
<h1>"Search results for " {query}</h1> <h1>"Search results for " {query}</h1>
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
{move || { {move || {
search_results search_results
.get() .get()
.map(move |search_results| { .map(move |search_results| {
let is_empty = search_results.is_empty(); let is_empty = search_results.is_empty();
view! { view! {
<Show <Show
when=move || !is_empty when=move || !is_empty
fallback=move || { fallback=move || {
let error_view = move || { let error_view = move || {
error error
.get() .get()
.map(|err| { .map(|err| {
view! { <p style="color:red;">{err}</p> } view! { <p style="color:red;">{err}</p> }
}) })
}; };
view! { view! {
{error_view} {error_view}
<p>No results found</p> <p>No results found</p>
} }
}
>
<ul>
// render resolved instance
{if let Some(instance) = &search_results.instance {
let domain = &instance.domain;
vec![
view! {
<li>
<a href=format!("/instance/{domain}")>{domain}</a>
</li>
},
]
} 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>
} }
> })
}}
<ul> </Suspense>
// render resolved instance
{if let Some(instance) = &search_results.instance {
let domain = &instance.domain;
vec![
view! {
<li>
<a href=format!("/instance/{domain}")>{domain}</a>
</li>
},
]
} 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,28 +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 || {
user_profile error
.get() .get()
.map(|person: DbPerson| { .map(|err| {
view! { view! { <p style="color:red;">{err}</p> }
<h1>{user_title(&person)}</h1>
<p>TODO: create actual user profile</p>
}
}) })
}} }}
</Suspense> <Suspense fallback=|| {
view! { "Loading..." }
}>
{move || {
user_profile
.get()
.map(|person: DbPerson| {
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 /> }
}); });
} }