//! Actions sidebar/menubar for profile page. use crate::Route; use crate::auth::AuthState; use crate::components::app_link::{AppLink, AppLinkTarget}; use crate::components::button::{Button, ButtonVariant}; use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; use crate::components::notebook_editor::{NotebookEditor, NotebookEditorMode, NotebookFormState}; use crate::fetch::Fetcher; use dioxus::prelude::*; use jacquard::IntoStatic; use jacquard::client::AgentSessionExt; use jacquard::types::ident::AtIdentifier; use jacquard::types::string::Datetime; use weaver_api::sh_weaver::actor::Author; use weaver_api::sh_weaver::notebook::book::Book; use weaver_common::slugify; const PROFILE_ACTIONS_CSS: Asset = asset!("/assets/styling/profile-actions.css"); /// Build a Book record from form state. fn build_book_from_form( form_state: &NotebookFormState, did: &jacquard::types::string::Did<'_>, path: String, ) -> Book<'static> { let now = Datetime::now(); let author = Author::new().did(did.clone().into_static()).build(); let title = form_state.title.clone(); let tags: Option> = if form_state.tags.is_empty() { None } else { Some(form_state.tags.iter().map(|s| s.clone().into()).collect()) }; Book::new() .authors(vec![author]) .entry_list(vec![]) .maybe_title(Some(title.into())) .maybe_path(Some(path.into())) .maybe_publish_global(Some(form_state.publish_global)) .maybe_tags(tags) .created_at(now.clone()) .updated_at(now) .build() } /// Actions available on the profile page for the owner. #[component] pub fn ProfileActions(ident: ReadSignal>) -> Element { let auth_state = use_context::>(); let fetcher = use_context::(); let navigator = use_navigator(); // State for the create notebook dialog. let mut show_create_dialog = use_signal(|| false); let mut saving = use_signal(|| false); let mut error = use_signal(|| None::); // Check if viewing own profile. let is_owner = { let current_did = auth_state.read().did.clone(); match (¤t_did, ident()) { (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, _ => false, } }; if !is_owner { return rsx! {}; } let create_fetcher = fetcher.clone(); let handle_save = move |form_state: NotebookFormState| { let fetcher = create_fetcher.clone(); let navigator = navigator.clone(); let ident_value = ident(); spawn(async move { saving.set(true); error.set(None); let did = match fetcher.current_did().await { Some(d) => d, None => { error.set(Some("Not authenticated".to_string())); saving.set(false); return; } }; let path = if form_state.path.is_empty() { slugify(&form_state.title) } else { form_state.path.clone() }; let book = build_book_from_form(&form_state, &did, path.clone()); match fetcher.create_record(book, None).await { Ok(_output) => { // TODO: If publish_global, create site.standard.publication record (Task 8). show_create_dialog.set(false); saving.set(false); navigator.push(Route::NotebookIndex { ident: ident_value, book_title: path.into(), }); } Err(e) => { error.set(Some(format!("Failed to create notebook: {:?}", e))); saving.set(false); } } }); }; let handle_cancel = move |_| { show_create_dialog.set(false); error.set(None); }; rsx! { document::Link { rel: "stylesheet", href: PROFILE_ACTIONS_CSS } aside { class: "profile-actions", div { class: "profile-actions-container", div { class: "profile-actions-list", AppLink { to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, class: "profile-action-link".to_string(), Button { variant: ButtonVariant::Outline, "New Entry" } } Button { variant: ButtonVariant::Outline, onclick: move |_| show_create_dialog.set(true), "New Notebook" } AppLink { to: AppLinkTarget::Drafts { ident: ident() }, class: "profile-action-link".to_string(), Button { variant: ButtonVariant::Ghost, "Drafts" } } AppLink { to: AppLinkTarget::Invites { ident: ident() }, class: "profile-action-link".to_string(), Button { variant: ButtonVariant::Ghost, "Invites" } } } } } // Create notebook dialog. DialogRoot { open: show_create_dialog(), on_open_change: move |open: bool| show_create_dialog.set(open), DialogContent { DialogTitle { "Create Notebook" } DialogDescription { "Create a new notebook to organize your entries." } NotebookEditor { mode: NotebookEditorMode::Create, on_save: handle_save, on_cancel: handle_cancel, saving: saving(), error: error(), } } } } } /// Mobile-friendly menubar version of profile actions. #[component] pub fn ProfileActionsMenubar(ident: ReadSignal>) -> Element { let auth_state = use_context::>(); let fetcher = use_context::(); let navigator = use_navigator(); let mut show_create_dialog = use_signal(|| false); let mut saving = use_signal(|| false); let mut error = use_signal(|| None::); let is_owner = { let current_did = auth_state.read().did.clone(); match (¤t_did, ident()) { (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, _ => false, } }; if !is_owner { return rsx! {}; } let create_fetcher = fetcher.clone(); let handle_save = move |form_state: NotebookFormState| { let fetcher = create_fetcher.clone(); let navigator = navigator.clone(); let ident_value = ident(); spawn(async move { saving.set(true); error.set(None); let did = match fetcher.current_did().await { Some(d) => d, None => { error.set(Some("Not authenticated".to_string())); saving.set(false); return; } }; let path = if form_state.path.is_empty() { slugify(&form_state.title) } else { form_state.path.clone() }; let book = build_book_from_form(&form_state, &did, path.clone()); match fetcher.create_record(book, None).await { Ok(_output) => { // TODO: If publish_global, create site.standard.publication record (Task 8). show_create_dialog.set(false); saving.set(false); navigator.push(Route::NotebookIndex { ident: ident_value, book_title: path.into(), }); } Err(e) => { error.set(Some(format!("Failed to create notebook: {:?}", e))); saving.set(false); } } }); }; let handle_cancel = move |_| { show_create_dialog.set(false); error.set(None); }; rsx! { div { class: "profile-actions-menubar", AppLink { to: AppLinkTarget::NewDraft { ident: ident(), notebook: None }, Button { variant: ButtonVariant::Primary, "New Entry" } } Button { variant: ButtonVariant::Outline, onclick: move |_| show_create_dialog.set(true), "New Notebook" } AppLink { to: AppLinkTarget::Drafts { ident: ident() }, Button { variant: ButtonVariant::Ghost, "Drafts" } } AppLink { to: AppLinkTarget::Invites { ident: ident() }, Button { variant: ButtonVariant::Ghost, "Invites" } } } // Create notebook dialog. DialogRoot { open: show_create_dialog(), on_open_change: move |open: bool| show_create_dialog.set(open), DialogContent { DialogTitle { "Create Notebook" } DialogDescription { "Create a new notebook to organize your entries." } NotebookEditor { mode: NotebookEditorMode::Create, on_save: handle_save, on_cancel: handle_cancel, saving: saving(), error: error(), } } } } }