Skip to content

Commit

Permalink
polish(web): lens edit UI (#465)
Browse files Browse the repository at this point in the history
* creating a dashboard page for signed in users and moving lenses out of navbar

* adding home button to nav bar

* splitting add source into it's own component

* continuing to move more stuff over to add_source component

* cleaning up source list & consolidate error msgs into an error bar at top

* use danger button type for delete button

* consolidating code for adding url

* consolidating add_source

* disable adding sources while we're in the middle of adding

* limit the size of the paginator

* show display name saving status & no need to pull entire lens data again

* refresh source list if we detect something is in progress

* reload lens data when a new source is successfully added

* clear inputs after add source

* cargo fmt + clippy
  • Loading branch information
a5huynh committed May 27, 2023
1 parent a1d4f23 commit c651a84
Show file tree
Hide file tree
Showing 12 changed files with 1,120 additions and 722 deletions.
1 change: 1 addition & 0 deletions apps/web/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ pub enum LensAddDocType {
GDrive {
token: String,
},
RssFeed,
/// Normal, web accessible URL.
WebUrl {
include_all_suburls: bool,
Expand Down
24 changes: 8 additions & 16 deletions apps/web/src/components/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ui_components::{
btn::{Btn, BtnSize, BtnType},
btn::{Btn, BtnSize},
icons,
};
use yew::prelude::*;
Expand All @@ -11,7 +11,6 @@ pub mod nav;

#[derive(Properties, PartialEq)]
pub struct LensListProps {
pub current: Option<String>,
pub lenses: Option<Vec<Lens>>,
#[prop_or_default]
pub on_select: Callback<Lens>,
Expand Down Expand Up @@ -40,18 +39,10 @@ pub fn lens_list(props: &LensListProps) -> Html {
props.class.clone(),
);

let current_lens = props.current.clone().unwrap_or_default();
let mut html = Vec::new();
let lenses = props.lenses.clone();
for lens in lenses.unwrap_or_default() {
let classes = classes!(
default_classes.clone(),
if current_lens == lens.name {
Some("bg-cyan-800")
} else {
None
}
);
let classes = classes!(default_classes.clone(),);

let onclick = {
let navi = navigator.clone();
Expand Down Expand Up @@ -90,22 +81,23 @@ pub fn lens_list(props: &LensListProps) -> Html {
html! {}
} else {
html! {
<Btn size={BtnSize::Sm} _type={BtnType::Borderless} classes="ml-auto rounded" onclick={on_edit}>
<Btn size={BtnSize::Sm} classes="rounded" onclick={on_edit}>
<icons::PencilIcon height="h-3" width="w-3" />
<span>{"Edit"}</span>
</Btn>
}
};

html.push(html! {
<li class="mb-1 flex flex-row items-center">
<li class="flex flex-row items-center justify-between gap-4">
<a class={classes.clone()} {onclick}>
{icon}
<div class="truncate text-ellipsis">{lens.display_name.clone()}</div>
{edit_icon}
<div class="truncate text-ellipsis text-lg">{lens.display_name.clone()}</div>
</a>
{edit_icon}
</li>
});
}

html! { <ul>{html}</ul> }
html! { <ul class="flex flex-col gap-2">{html}</ul> }
}
98 changes: 18 additions & 80 deletions apps/web/src/components/nav.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
use ui_components::btn::{Btn, BtnSize, BtnType};
use ui_components::icons;
use yew::{platform::spawn_local, prelude::*};
use yew_router::prelude::use_navigator;

use super::LensList;
use crate::client::Lens;
use crate::metrics::{Metrics, WebClientEvent};
use crate::{auth0_login, auth0_logout, AuthStatus, Route};
use crate::{auth0_login, auth0_logout, AuthStatus};

#[derive(Properties, PartialEq)]
pub struct NavBarProps {
pub current_lens: Option<String>,
#[prop_or_default]
pub on_create_lens: Callback<Lens>,
#[prop_or_default]
pub on_select_lens: Callback<Lens>,
#[prop_or_default]
pub on_edit_lens: Callback<Lens>,
pub session_uuid: String,
}

#[function_component(NavBar)]
pub fn nav_bar_component(props: &NavBarProps) -> Html {
let navigator = use_navigator().expect("Navigator not available");
let auth_status = use_context::<AuthStatus>().expect("Ctxt not set up");
let user_data = auth_status.user_data.clone();
let toggle_nav = use_state(|| false);
let metrics = Metrics::new(false);
let uuid = props.session_uuid.clone();
Expand All @@ -51,27 +40,6 @@ pub fn nav_bar_component(props: &NavBarProps) -> Html {
});
});

let auth_status_handle = auth_status.clone();
let on_create = props.on_create_lens.clone();
let create_lens_cb = Callback::from(move |_| {
let navigator = navigator.clone();
let auth_status_handle: AuthStatus = auth_status_handle.clone();
let on_create = on_create.clone();
spawn_local(async move {
// create a new lens
let api = auth_status_handle.get_client();
match api.lens_create().await {
Ok(new_lens) => {
on_create.emit(new_lens.clone());
navigator.push(&Route::Edit {
lens: new_lens.name,
})
}
Err(err) => log::error!("error creating lens: {err}"),
}
});
});

#[cfg(debug_assertions)]
let debug_vars = html! {
<>
Expand Down Expand Up @@ -113,15 +81,14 @@ pub fn nav_bar_component(props: &NavBarProps) -> Html {
{ if *toggle_nav {
html! {
<div class="w-full block flex-grow lg:flex lg:items-center lg:w-auto pt-4">
{if let Some(user_data) = &user_data {
html!{
<LensList
class="text-lg"
current={props.current_lens.clone()}
lenses={user_data.lenses.clone()}
on_select={props.on_select_lens.clone()}
on_edit={props.on_edit_lens.clone()}
/>
{if auth_status.is_authenticated {
html! {
<div>
<a href="/" class="p-2 flex flex-row text-lg items-center gap-2 rounded hover:bg-neutral-500">
<icons::HomeIcon />
<span>{"Home"}</span>
</a>
</div>
}
} else {
html! {
Expand All @@ -136,7 +103,7 @@ pub fn nav_bar_component(props: &NavBarProps) -> Html {
</div>
<div class="text-white hidden sm:block w-48 xl:w-64 min-w-max bg-stone-900 p-4 top-0 left-0 z-40 sticky h-screen">
<a href="/" class="cursor-pointer"><img src="/icons/logo@2x.png" class="w-12 h-12 mx-auto" /></a>
<div class="my-6">
<div>
{if auth_status.is_authenticated {
if let Some(profile) = auth_status.user_profile {
html! {
Expand All @@ -161,46 +128,17 @@ pub fn nav_bar_component(props: &NavBarProps) -> Html {
}
}}
</div>
<div class="mb-6">
<div class="uppercase mb-2 text-xs text-gray-500 font-bold">
{"My Lenses"}
</div>
{if auth_status.is_authenticated {
html! {
<Btn size={BtnSize::Sm} classes="mb-2 w-full" onclick={create_lens_cb.clone()}>
<icons::PlusIcon width="w-4" height="h-4" />
<span>{"Create Lens"}</span>
</Btn>
}
} else {
html! {
<div class="text-neutral-400 text-xs">{"Please login to see your lenses"}</div>
}
}}
{if let Some(user_data) = &user_data {
html!{
<LensList
class="text-sm"
current={props.current_lens.clone()}
lenses={user_data.lenses.clone()}
on_select={props.on_select_lens.clone()}
on_edit={props.on_edit_lens.clone()}
/>
}
} else {
html! {}
}}
<hr class="border border-neutral-700 mt-6 mb-4" />
<div>
<a href="/" class="p-2 flex flex-row text-lg items-center gap-2 rounded hover:bg-neutral-500">
<icons::HomeIcon />
<span>{"Home"}</span>
</a>
</div>
<div class="hidden">
<div class="mt-4">
<div class="uppercase mb-2 text-xs text-gray-500 font-bold">
{"Searches"}
{"My Q&As"}
</div>
<ul>
<li class="mb-2">
<icons::GlobeIcon classes="mr-2" height="h-4" width="h-4" />
{"Search"}
</li>
</ul>
</div>
<div class="absolute text-xs text-neutral-600 bottom-0 py-4 flex flex-col">
<div>
Expand Down
24 changes: 17 additions & 7 deletions apps/web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod components;
mod metrics;
mod pages;
use components::nav::NavBar;
use pages::{landing::LandingPage, lens_edit::CreateLensPage, AppPage};
use pages::{dashboard::Dashboard, landing::LandingPage, lens_editor::CreateLensPage, AppPage};

use crate::{client::ApiClient, pages::search::SearchPage};

Expand Down Expand Up @@ -224,13 +224,27 @@ impl Component for App {
let switch = {
let link = link.clone();
let uuid = self.session_uuid.clone();
let is_authenticated = self.auth_status.is_authenticated;
move |routes: Route| match &routes {
Route::Start => {
html! { <AppPage><LandingPage session_uuid={uuid.clone()} /></AppPage> }
if is_authenticated {
html! {
<AppPage>
<Dashboard
session_uuid={uuid.clone()}
on_create_lens={handle_on_create_lens.clone()}
on_select_lens={link.callback(Msg::SetSelectedLens)}
on_edit_lens={link.callback(Msg::SetSelectedLens)}
/>
</AppPage>
}
} else {
html! { <AppPage><LandingPage session_uuid={uuid.clone()} /></AppPage> }
}
}
Route::Edit { lens } => html! {
<AppPage>
<CreateLensPage lens={lens.clone()} onupdate={link.callback(|_| Msg::LoadLenses)} />
<CreateLensPage lens={lens.clone()} />
</AppPage>
},
Route::Search { lens } => {
Expand All @@ -255,10 +269,6 @@ impl Component for App {
<NavBar
current_lens={self.current_lens.clone()}
session_uuid={self.session_uuid.clone()}
// Reload lenses after a new one is created.
on_create_lens={handle_on_create_lens.clone()}
on_select_lens={link.callback(Msg::SetSelectedLens)}
on_edit_lens={link.callback(Msg::SetSelectedLens)}
/>
<Switch<Route> render={switch} />
</div>
Expand Down
108 changes: 108 additions & 0 deletions apps/web/src/pages/dashboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use ui_components::btn::{Btn, BtnSize, BtnType};
use ui_components::icons;
use yew::{platform::spawn_local, prelude::*};
use yew_router::prelude::use_navigator;

use crate::components::LensList;
use crate::{client::Lens, AuthStatus, Route};

#[derive(Properties, PartialEq)]
pub struct DashboardProps {
pub session_uuid: String,
#[prop_or_default]
pub on_create_lens: Callback<Lens>,
#[prop_or_default]
pub on_select_lens: Callback<Lens>,
#[prop_or_default]
pub on_edit_lens: Callback<Lens>,
}

#[function_component(Dashboard)]
pub fn landing_page(props: &DashboardProps) -> Html {
let navigator = use_navigator().expect("Navigator not available");
let auth_status = use_context::<AuthStatus>().expect("ctx not setup");

let user_data = auth_status.user_data.clone();

let create_lens_cb = {
let auth_status_handle = auth_status;
let on_create = props.on_create_lens.clone();
Callback::from(move |_: MouseEvent| {
let navigator = navigator.clone();
let auth_status_handle: AuthStatus = auth_status_handle.clone();
let on_create = on_create.clone();
spawn_local(async move {
// create a new lens
let api = auth_status_handle.get_client();
match api.lens_create().await {
Ok(new_lens) => {
on_create.emit(new_lens.clone());
navigator.push(&Route::Edit {
lens: new_lens.name,
})
}
Err(err) => log::error!("error creating lens: {err}"),
}
});
})
};

if let Some(user_data) = user_data {
html! {
<div class="p-8">
<div class="flex flex-row items-center mb-2 justify-between">
<div class="uppercase text-xs text-gray-500 font-bold">
{"My Lenses"}
</div>
<Btn size={BtnSize::Xs} _type={BtnType::Primary} onclick={create_lens_cb.clone()}>
<icons::PlusIcon width="w-4" height="h-4" />
<span>{"Create New"}</span>
</Btn>
</div>
<LensList
class="text-sm"
lenses={user_data.lenses.clone()}
on_select={props.on_select_lens.clone()}
on_edit={props.on_edit_lens.clone()}
/>
</div>
}
} else {
html! {}
}
}

#[derive(Properties, PartialEq)]
struct PublicExampleProps {
href: String,
name: String,
description: String,
sources: Vec<String>,
}

#[function_component(PublicExample)]
fn pub_example(props: &PublicExampleProps) -> Html {
let sources = props
.sources
.iter()
.map(|source| {
html! {
<span class="ml-2 underline text-cyan-500">{source}</span>
}
})
.collect::<Html>();

html! {
<a
href={props.href.clone()}
class="flex flex-col justify-between border border-neutral-600 p-4 rounded-md hover:border-cyan-500 cursor-pointer"
>
<div class="pb-2">{props.name.clone()}</div>
<div class="text-sm text-neutral-400">{props.description.clone()}</div>
<div class="pt-4 text-xs mt-auto">
<span class="text-neutral-400">{"source:"}</span>
{sources}
</div>
</a>
}
}
Loading

0 comments on commit c651a84

Please sign in to comment.