This commit is contained in:
Felix Ableitner 2024-10-16 16:40:32 +02:00
parent 8033a00b4e
commit 68f7f31b8e
10 changed files with 152 additions and 94 deletions

View File

@ -3,20 +3,34 @@ use crate::{
frontend::{ frontend::{
app::GlobalState, app::GlobalState,
article_link, article_link,
article_title,
components::instance_follow_button::InstanceFollowButton, components::instance_follow_button::InstanceFollowButton,
}, },
}; };
use leptos::*; use leptos::*;
use leptos_router::*; use leptos_router::*;
pub enum ActiveTab {
Read,
History,
Edit,
Actions,
}
#[component] #[component]
pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoView { pub fn ArticleNav(
article: Resource<Option<String>, ArticleView>,
active_tab: ActiveTab,
) -> impl IntoView {
let tab_classes = tab_classes(&active_tab);
view! { view! {
<Suspense> <Suspense>
{move || { {move || {
article article
.get() .get()
.map(|article_| { .map(|article_| {
let title = article_title(&article_.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 {
@ -32,8 +46,12 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
let protected = article_.article.protected; let protected = article_.article.protected;
view! { view! {
<div role="tablist" class="tabs tabs-lifted"> <div role="tablist" class="tabs tabs-lifted">
<A class="tab tab-active" href=article_link.clone()>"Read"</A> <A class=tab_classes.read href=article_link.clone()>
<A class="tab" href=format!("{article_link}/history")>"History"</A> "Read"
</A>
<A class=tab_classes.history href=format!("{article_link}/history")>
"History"
</A>
<Show when=move || { <Show when=move || {
global_state global_state
.with(|state| { .with(|state| {
@ -46,12 +64,19 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
&& can_edit_article(&article_.article, is_admin).is_ok() && can_edit_article(&article_.article, is_admin).is_ok()
}) })
}> }>
<A class="tab" href=format!("{article_link}/edit")>"Edit"</A> <A class=tab_classes.edit href=format!("{article_link}/edit")>
"Edit"
</A>
</Show> </Show>
<Show when=move || { <Show when=move || {
global_state.with(|state| state.my_profile.is_some()) global_state.with(|state| state.my_profile.is_some())
}> }>
<A class="tab" href=format!("{article_link_}/actions")>"Actions"</A> <A
class=tab_classes.actions
href=format!("{article_link_}/actions")
>
"Actions"
</A>
{instance {instance
.get() .get()
.map(|i| { .map(|i| {
@ -61,6 +86,9 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
})} })}
</Show> </Show>
</div>
<div class="flex flex-row">
<h1 class="text-4xl font-bold font-serif my-4 grow max-w-fit">{title}</h1>
<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"
@ -74,3 +102,28 @@ pub fn ArticleNav(article: Resource<Option<String>, ArticleView>) -> impl IntoVi
</Suspense> </Suspense>
} }
} }
struct ActiveTabClasses {
read: &'static str,
history: &'static str,
edit: &'static str,
actions: &'static str,
}
fn tab_classes(active_tab: &ActiveTab) -> ActiveTabClasses {
const TAB_INACTIVE: &str = "tab";
const TAB_ACTIVE: &str = "tab tab-active";
let mut classes = ActiveTabClasses {
read: TAB_INACTIVE,
history: TAB_INACTIVE,
edit: TAB_INACTIVE,
actions: TAB_INACTIVE,
};
match active_tab {
ActiveTab::Read => classes.read = TAB_ACTIVE,
ActiveTab::History => classes.history = TAB_ACTIVE,
ActiveTab::Edit => classes.edit = TAB_ACTIVE,
ActiveTab::Actions => classes.actions = TAB_ACTIVE,
}
classes
}

View File

@ -24,12 +24,13 @@ pub fn CredentialsForm(
error error
.get() .get()
.map(|err| { .map(|err| {
view! { <p style="color:red;">{err}</p> } view! { <p class="alert alert-error">{err}</p> }
}) })
}} }}
<input <input
type="text" type="text"
class="input"
required required
placeholder="Username" placeholder="Username"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
@ -46,6 +47,7 @@ pub fn CredentialsForm(
<input <input
type="password" type="password"
class="input"
required required
placeholder="Password" placeholder="Password"
prop:disabled=move || disabled.get() prop:disabled=move || disabled.get()
@ -69,6 +71,7 @@ pub fn CredentialsForm(
<div> <div>
<button <button
class="btn"
prop:disabled=move || button_is_disabled.get() prop:disabled=move || button_is_disabled.get()
on:click=move |_| dispatch_action() on:click=move |_| dispatch_action()
> >

View File

@ -47,7 +47,8 @@ pub fn Nav() -> impl IntoView {
if !query.is_empty() { if !query.is_empty() {
navigate(&format!("/search?query={query}"), Default::default()); navigate(&format!("/search?query={query}"), Default::default());
} }
}> }
>
<input <input
type="text" type="text"
class="input input-secondary input-bordered input-xs w-full rounded" class="input input-secondary input-bordered input-xs w-full rounded"
@ -84,14 +85,13 @@ pub fn Nav() -> impl IntoView {
let profile_link = format!("/user/{}", my_profile.person.username); let profile_link = format!("/user/{}", my_profile.person.username);
view! { view! {
<p class="self-center pb-2"> <p class="self-center pb-2">
"Logged in as " "Logged in as " <a class="link" href=profile_link>
<a class="link"
href=profile_link
>
{my_profile.person.username} {my_profile.person.username}
</a> </a>
</p> </p>
<button class="btn" on:click=move |_| logout_action.dispatch(())>Logout</button> <button class="btn" on:click=move |_| logout_action.dispatch(())>
Logout
</button>
} }
} }

View File

@ -52,7 +52,11 @@ fn user_link(person: &DbPerson) -> impl IntoView {
extract_domain(&person.ap_id) extract_domain(&person.ap_id)
) )
}; };
view! { <a href=creator_path>{user_title(person)}</a> } view! {
<a class="link" href=creator_path>
{user_title(person)}
</a>
}
} }
fn render_date_time(date_time: DateTime<Utc>) -> String { fn render_date_time(date_time: DateTime<Utc>) -> String {

View File

@ -3,8 +3,7 @@ use crate::{
frontend::{ frontend::{
app::GlobalState, app::GlobalState,
article_link, article_link,
article_title, components::article_nav::{ActiveTab, ArticleNav},
components::article_nav::ArticleNav,
pages::article_resource, pages::article_resource,
DbArticle, DbArticle,
}, },
@ -52,7 +51,7 @@ pub fn ArticleActions() -> impl IntoView {
} }
}); });
view! { view! {
<ArticleNav article=article /> <ArticleNav article=article active_tab=ActiveTab::Actions />
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
@ -61,16 +60,14 @@ pub fn ArticleActions() -> impl IntoView {
.get() .get()
.map(|article| { .map(|article| {
view! { view! {
<div class="item-view"> <div>
<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 class="alert">{err}</p> }
}) })
}} }}
<Show when=move || { <Show when=move || {
global_state global_state
.with(|state| { .with(|state| {
@ -81,14 +78,19 @@ pub fn ArticleActions() -> impl IntoView {
.unwrap_or_default() && article.article.local .unwrap_or_default() && article.article.local
}) })
}> }>
<button on:click=move |_| { <button
class="btn btn-secondary"
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
class="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);
@ -97,6 +99,7 @@ pub fn ArticleActions() -> impl IntoView {
/> />
<button <button
class="btn"
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()))

View File

@ -2,8 +2,7 @@ use crate::{
common::{ApiConflict, ArticleView, EditArticleForm}, common::{ApiConflict, ArticleView, EditArticleForm},
frontend::{ frontend::{
app::GlobalState, app::GlobalState,
article_title, components::article_nav::{ActiveTab, ArticleNav},
components::article_nav::ArticleNav,
markdown::render_markdown, markdown::render_markdown,
pages::article_resource, pages::article_resource,
}, },
@ -103,7 +102,7 @@ pub fn EditArticle() -> impl IntoView {
); );
view! { view! {
<ArticleNav article=article /> <ArticleNav article=article active_tab=ActiveTab::Edit />
<Show <Show
when=move || edit_response.get() == EditResponse::Success when=move || edit_response.get() == EditResponse::Success
fallback=move || { fallback=move || {
@ -126,7 +125,6 @@ pub fn EditArticle() -> impl IntoView {
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> <div>
<h1>{article_title(&article.article)}</h1>
{move || { {move || {
edit_error edit_error
.get() .get()
@ -134,7 +132,6 @@ pub fn EditArticle() -> impl IntoView {
view! { <p style="color:red;">{err}</p> } view! { <p style="color:red;">{err}</p> }
}) })
}} }}
<textarea <textarea
id="edit-article-textarea" id="edit-article-textarea"
class="textarea textarea-bordered textarea-primary min-w-full" class="textarea textarea-bordered textarea-primary min-w-full"
@ -147,19 +144,19 @@ pub fn EditArticle() -> impl IntoView {
> >
{article.article.text.clone()} {article.article.text.clone()}
</textarea> </textarea>
<button class="btn" on:click=move |_| { <button
set_show_preview.update(|s| *s = !*s) class="btn"
}>Preview</button> on:click=move |_| { set_show_preview.update(|s| *s = !*s) }
<Show when=move || { show_preview.get() }> >
Preview
</button> <Show when=move || { show_preview.get() }>
<div class="preview" inner_html=move || preview.get()></div> <div class="preview" inner_html=move || preview.get()></div>
</Show> </Show> <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> <div class="inputs">
<div class="inputs">
<input <input
type="text" type="text"
class="input input-secondary" class="input input-secondary"

View File

@ -1,6 +1,5 @@
use crate::frontend::{ use crate::frontend::{
article_title, components::article_nav::{ActiveTab, ArticleNav},
components::article_nav::ArticleNav,
extract_domain, extract_domain,
pages::article_resource, pages::article_resource,
render_date_time, render_date_time,
@ -13,7 +12,7 @@ pub fn ArticleHistory() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article /> <ArticleNav article=article active_tab=ActiveTab::History />
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
@ -22,9 +21,8 @@ pub fn ArticleHistory() -> impl IntoView {
.get() .get()
.map(|article| { .map(|article| {
view! { view! {
<div class="item-view"> <div>
<h1>{article_title(&article.article)}</h1> <ul class="list-disc">
{article {article
.edits .edits
.into_iter() .into_iter()
@ -39,13 +37,14 @@ pub fn ArticleHistory() -> impl IntoView {
view! { view! {
<li> <li>
{render_date_time(edit.edit.created)}": " {render_date_time(edit.edit.created)}": "
<a href=path>{edit.edit.summary}</a> " by " <a class="link link-primary" href=path>
{user_link(&edit.creator)} {edit.edit.summary}
</a> " by " {user_link(&edit.creator)}
</li> </li>
} }
}) })
.collect::<Vec<_>>()} .collect::<Vec<_>>()}
</ul>
</div> </div>
} }
}) })

View File

@ -21,19 +21,19 @@ pub fn ListArticles() -> impl IntoView {
); );
view! { view! {
<h1>Most recently edited Articles</h1> <h1 class="text-4xl font-bold font-serif my-4">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" class="radio radio-primary" />
<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" class="radio radio-primary" checked />
<label for="all">All</label> <label for="all">All</label>
</fieldset> </fieldset>
<ul> <ul class="list-disc">
{move || { {move || {
articles articles
.get() .get()
@ -42,7 +42,7 @@ pub fn ListArticles() -> impl IntoView {
.map(|a| { .map(|a| {
view! { view! {
<li> <li>
<a href=article_link(&a)>{article_title(&a)}</a> <a class="link link-secondary" href=article_link(&a)>{article_title(&a)}</a>
</li> </li>
} }
}) })

View File

@ -1,6 +1,5 @@
use crate::frontend::{ use crate::frontend::{
article_title, components::article_nav::{ActiveTab, ArticleNav},
components::article_nav::ArticleNav,
markdown::render_markdown, markdown::render_markdown,
pages::article_resource, pages::article_resource,
}; };
@ -11,7 +10,7 @@ pub fn ReadArticle() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article /> <ArticleNav article=article active_tab=ActiveTab::Read />
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>
@ -21,10 +20,10 @@ pub fn ReadArticle() -> impl IntoView {
.get() .get()
.map(|article| { .map(|article| {
view! { view! {
<div class="prose prose-slate"> <div
<h1 class="slate">{article_title(&article.article)}</h1> class="prose prose-slate"
<div inner_html=render_markdown(&article.article.text)></div> inner_html=render_markdown(&article.article.text)
</div> ></div>
} }
}) })
}} }}

View File

@ -1,5 +1,5 @@
use crate::frontend::{ use crate::frontend::{
components::article_nav::ArticleNav, components::article_nav::{ActiveTab, ArticleNav},
pages::article_resource, pages::article_resource,
render_date_time, render_date_time,
user_link, user_link,
@ -13,7 +13,7 @@ pub fn EditDiff() -> impl IntoView {
let article = article_resource(); let article = article_resource();
view! { view! {
<ArticleNav article=article /> <ArticleNav article=article active_tab=ActiveTab::Edit />
<Suspense fallback=|| { <Suspense fallback=|| {
view! { "Loading..." } view! { "Loading..." }
}> }>