diff --git a/assets/favicon.ico b/assets/favicon.ico index 070d1da..adc9155 100644 Binary files a/assets/favicon.ico and b/assets/favicon.ico differ diff --git a/assets/images/icon.png b/assets/images/icon.png index 1c33866..8ea72ef 100644 Binary files a/assets/images/icon.png and b/assets/images/icon.png differ diff --git a/assets/images/icon.svg b/assets/images/icon.svg deleted file mode 100644 index 30bb299..0000000 --- a/assets/images/icon.svg +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/styles/input_range.css b/assets/styles/input_range.css index a9338cd..1de33c2 100644 --- a/assets/styles/input_range.css +++ b/assets/styles/input_range.css @@ -15,6 +15,7 @@ input[type="range"]::-moz-range-thumb, input[type="range"]::-webkit-slider-thumb filter: drop-shadow(0 var(--spacing) 0 var(--color-gray-500)); border: 0; border-radius: 0.5rem; + cursor: pointer; } input[type="range"]::-webkit-slider-thumb { diff --git a/src/components/app.rs b/src/components/app.rs index 9bbf89b..6e4204c 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -1,4 +1,5 @@ use crate::internationalization::get_language_identifier; + use crate::route::Route; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; @@ -41,7 +42,7 @@ pub(crate) fn App() -> Element { document::Link { rel: "manifest", href: MANIFEST, crossorigin: "use-credentials" } div { - class: "min-h-screen pt-4 pb-36 flex flex-col text-gray-300 bg-gray-900", + class: "min-h-screen py-4 flex flex-col text-gray-300 bg-gray-900", Router:: {} } } diff --git a/src/components/bottom_panel.rs b/src/components/bottom_panel.rs index 6d2f0b0..f230871 100644 --- a/src/components/bottom_panel.rs +++ b/src/components/bottom_panel.rs @@ -1,78 +1,22 @@ -use crate::components::error_boundary_message::ErrorBoundaryMessage; use crate::components::navigation::Navigation; -use crate::components::project_form::ProjectForm; -use crate::components::task_form::TaskForm; -use crate::models::project::Project; -use crate::models::task::Task; -use crate::route::Route; use dioxus::prelude::*; #[component] -pub(crate) fn BottomPanel(display_form: Signal) -> Element { - // A signal for delaying the application of styles. - #[allow(clippy::redundant_closure)] - let mut expanded = use_signal(|| display_form()); +pub(crate) fn BottomPanel() -> Element { let navigation_expanded = use_signal(|| false); - let current_route = use_route(); - - let mut project_being_edited = use_context::>>(); - let mut task_being_edited = use_context::>>(); - - use_effect(use_reactive(&display_form, move |display_form| { - if display_form() { - expanded.set(true); - } else { - spawn(async move { - // Necessary for a smooth – not instant – height transition. - #[cfg(not(feature = "server"))] - async_std::task::sleep(std::time::Duration::from_millis(500)).await; - /* The check is necessary for the situation when the user expands the panel while - it is being closed. */ - if !display_form() { - expanded.set(false); - } - }); - } - })); rsx! { div { class: format!( "flex flex-col pointer-events-auto bg-gray-800 transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] overflow-y-scroll {}", - match (display_form(), current_route, navigation_expanded()) { - (false, _, false) => "h-[66px]", - (false, _, true) => "h-[130px]", - (true, Route::ProjectsPage, _) => "h-[130px]", - (true, _, _) => "h-[506px]", + if navigation_expanded() { + "h-[130px]" + } else { + "h-[66px]" } ), - if expanded() { - ErrorBoundaryMessage { - match current_route { - Route::ProjectsPage => rsx! { - ProjectForm { - project: project_being_edited(), - on_successful_submit: move |_| { - display_form.set(false); - project_being_edited.set(None); - } - } - }, - _ => rsx! { - TaskForm { - task: task_being_edited(), - on_successful_submit: move |_| { - display_form.set(false); - task_being_edited.set(None); - } - } - } - } - } - } else { - Navigation { - is_expanded: navigation_expanded, - } + Navigation { + is_expanded: navigation_expanded, } } } diff --git a/src/components/button_primary.rs b/src/components/button_primary.rs new file mode 100644 index 0000000..7fe01c2 --- /dev/null +++ b/src/components/button_primary.rs @@ -0,0 +1,29 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn ButtonPrimary( + class: Option, + children: Element, + #[props(extends = GlobalAttributes, extends = button)] attributes: Vec, + // TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved. + onclick: Option>>, +) -> Element { + rsx! { + button { + class: format!( + "cursor-pointer pb-[6px] hover:pb-[7px] active:pb-[2px] mt-[1px] hover:mt-0 active:mt-[5px] hover:*:drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] active:*:drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] transition-all duration-150 {}", + class.unwrap_or("".to_owned()) + ), + onclick: move |event| { + if let Some(onclick) = onclick { + onclick.call(event); + } + }, + ..attributes, + div { + class: "py-3.5 px-4 flex flex-row justify-center items-center bg-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] text-amber-700-muted rounded-xl transition-all duration-150", + {children} + } + } + } +} diff --git a/src/components/button_secondary.rs b/src/components/button_secondary.rs new file mode 100644 index 0000000..4be48a0 --- /dev/null +++ b/src/components/button_secondary.rs @@ -0,0 +1,29 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn ButtonSecondary( + class: Option, + children: Element, + #[props(extends = GlobalAttributes, extends = button)] attributes: Vec, + // TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved. + onclick: Option>>, +) -> Element { + rsx! { + button { + class: format!( + "cursor-pointer pb-[6px] hover:pb-[7px] active:pb-[2px] mt-[1px] hover:mt-0 active:mt-[5px] hover:*:drop-shadow-[0_7px_0_var(--color-gray-800)] active:*:drop-shadow-[0_2px_0_var(--color-gray-800)] transition-all duration-150 {}", + class.unwrap_or("".to_owned()) + ), + onclick: move |event| { + if let Some(onclick) = onclick { + onclick.call(event); + } + }, + ..attributes, + div { + class: "py-3.5 px-4 flex flex-row justify-center items-center bg-gray-600 drop-shadow-[0_6px_0_var(--color-gray-800)] rounded-xl transition-all duration-150", + {children} + } + } + } +} diff --git a/src/components/category_calendar_task_list.rs b/src/components/category_calendar_task_list.rs index 5a61335..16df019 100644 --- a/src/components/category_calendar_task_list.rs +++ b/src/components/category_calendar_task_list.rs @@ -28,11 +28,12 @@ pub(crate) fn CategoryCalendarTaskList() -> Element { div { class: "flex flex-col gap-4", div { - class: "px-7 flex flex-row items-center gap-2 font-bold", + class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold", div { class: "pt-1", { - date_current.format_localized(t!( + date_current.format_localized( + t!( if date_current.year() == Local::now().year() { "date-weekday-format" } else { diff --git a/src/components/category_input.rs b/src/components/category_input.rs index 128a60a..9420271 100644 --- a/src/components/category_input.rs +++ b/src/components/category_input.rs @@ -3,9 +3,9 @@ use crate::models::category::Category; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; -use dioxus_free_icons::Icon; +use dioxus_free_icons::icons::fa_regular_icons::FaLightbulb; use dioxus_free_icons::icons::fa_solid_icons::{ - FaCalendarDays, FaForward, FaHourglassHalf, FaInbox, FaQuestion, FaSignsPost, FaWater + FaCalendarDays, FaHourglassHalf, FaInbox, FaSignsPost, FaWater, }; #[component] @@ -17,7 +17,7 @@ pub(crate) fn CategoryInput( div { class: format!("grid grid-cols-3 gap-3 {}", class.unwrap_or("")), SelectButton { - icon: FaQuestion, + icon: FaLightbulb, is_selected: matches!(selected_category(), Category::SomedayMaybe), on_select: move |_| { selected_category.set(Category::SomedayMaybe); diff --git a/src/components/create_button.rs b/src/components/create_button.rs new file mode 100644 index 0000000..a67576d --- /dev/null +++ b/src/components/create_button.rs @@ -0,0 +1,31 @@ +use crate::components::project_form::PROJECT_BEING_EDITED; +use crate::components::{button_primary::ButtonPrimary, task_form::TASK_BEING_EDITED}; +use crate::route::Route; +use dioxus::prelude::*; +use dioxus_free_icons::{Icon, icons::fa_solid_icons::FaGavel}; + +#[component] +pub(crate) fn CreateButton() -> Element { + let navigator = use_navigator(); + let current_route = use_route(); + rsx! { + ButtonPrimary { + class: "pointer-events-auto m-4 self-end *:rounded-full! *:p-4", + onclick: move |_| { + *TASK_BEING_EDITED.write() = None; + *PROJECT_BEING_EDITED.write() = None; + navigator.push( + match current_route { + Route::ProjectsPage => Route::ProjectFormPage, + _ => Route::TaskFormPage, + } + ); + }, + Icon { + icon: FaGavel, + height: 24, + width: 24 + } + } + } +} diff --git a/src/components/form_open_button.rs b/src/components/form_open_button.rs deleted file mode 100644 index 6142867..0000000 --- a/src/components/form_open_button.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::models::project::Project; -use crate::models::task::Task; -use dioxus::prelude::*; -use dioxus_free_icons::{ - Icon, - icons::fa_solid_icons::{FaGavel, FaPlus, FaXmark}, -}; - -#[component] -pub(crate) fn FormOpenButton(is_opened: Signal) -> Element { - let mut project_being_edited = use_context::>>(); - let mut task_being_edited = use_context::>>(); - - rsx! { - button { - class: "pointer-events-auto m-4 p-4 self-end bg-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] rounded-full text-amber-700-muted cursor-pointer", - onclick: move |_| { - if is_opened() { - project_being_edited.set(None); - task_being_edited.set(None); - } - is_opened.set(!is_opened()); - }, - if is_opened() { - Icon { - icon: FaXmark, - height: 24, - width: 24 - } - } else { - Icon { - icon: FaGavel, - height: 24, - width: 24 - } - } - } - } -} diff --git a/src/components/input.rs b/src/components/input.rs index 72e434a..8a7d4d3 100644 --- a/src/components/input.rs +++ b/src/components/input.rs @@ -2,41 +2,46 @@ use dioxus::prelude::*; #[component] pub(crate) fn Input( - name: String, - required: Option, - disabled: Option, - initial_value: Option, class: Option, + name: String, r#type: String, - inputmode: Option, - min: Option, + id: Option, + #[props(extends = GlobalAttributes, extends = input)] attributes: Vec, + // TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/5271 gets resolved. autofocus: Option, + // TODO: Remove this once https://github.com/DioxusLabs/dioxus/issues/4019 gets resolved. oninput: Option>>, + onchange: Option>>, ) -> Element { rsx! { input { - name: name.clone(), - required, - disabled, - initial_value, class: format!( - "pt-3 pb-2.25 {} grow bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl {}", + "pt-3 pb-2.25 {} bg-gray-800-muted enabled:hover:bg-gray-800 enabled:focus:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl outline-0 {} transition-all duration-150 {}", match r#type.as_str() { "date" => "ps-3.25 pe-3", _ => "px-4" }, + match r#type.as_str() { + "text" | "number" => "", + _ => "enabled:cursor-pointer" + }, class.unwrap_or("".to_owned()) ), + name: name.clone(), r#type, - inputmode, - min, + id: id.unwrap_or(format!("input_{}", name)), autofocus, - id: "input_{name}", oninput: move |event| { if let Some(oninput) = oninput { oninput.call(event); } - } + }, + onchange: move |event| { + if let Some(onchange) = oninput { + onchange.call(event); + } + }, + ..attributes } } } diff --git a/src/components/input_label.rs b/src/components/input_label.rs index 6f5952c..b1b859d 100644 --- a/src/components/input_label.rs +++ b/src/components/input_label.rs @@ -4,12 +4,12 @@ use dioxus_free_icons::{Icon, IconShape}; #[component] pub(crate) fn InputLabel( icon: I, - r#for: Option + r#for: Option, ) -> Element { rsx! { label { r#for, - class: "mt-0.5 min-w-6 flex flex-row justify-center items-center", + class: "mt-0.5 min-w-7 flex flex-row justify-center items-center", Icon { class: "text-gray-600", icon, diff --git a/src/components/mod.rs b/src/components/mod.rs index 6deb488..4ca3745 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,10 +1,12 @@ pub(crate) mod app; pub(crate) mod bottom_panel; +pub(crate) mod button_primary; +pub(crate) mod button_secondary; pub(crate) mod category_calendar_task_list; pub(crate) mod category_input; pub(crate) mod category_today_task_list; +pub(crate) mod create_button; pub(crate) mod error_boundary_message; -pub(crate) mod form_open_button; pub(crate) mod input; pub(crate) mod input_label; pub(crate) mod navigation; diff --git a/src/components/navigation.rs b/src/components/navigation.rs index caeb75d..0ced100 100644 --- a/src/components/navigation.rs +++ b/src/components/navigation.rs @@ -2,9 +2,10 @@ use crate::components::navigation_item::NavigationItem; use crate::route::Route; use dioxus::prelude::*; use dioxus_free_icons::Icon; +use dioxus_free_icons::icons::fa_regular_icons::FaLightbulb; use dioxus_free_icons::icons::fa_solid_icons::{ - FaBars, FaCalendarDay, FaCalendarDays, FaCheck, FaForward, FaHourglassHalf, FaInbox, FaList, - FaQuestion, FaTrashCan, + FaBars, FaCalendarDay, FaCalendarDays, FaHourglassHalf, FaInbox, FaList, FaSignsPost, + FaTrashCan, FaVolcano, }; #[component] @@ -31,7 +32,7 @@ pub(crate) fn Navigation(is_expanded: Signal) -> Element { }, NavigationItem { route: Route::CategoryNextStepsPage, - icon: FaForward + icon: FaSignsPost }, NavigationItem { route: Route::CategoryCalendarPage, @@ -57,11 +58,11 @@ pub(crate) fn Navigation(is_expanded: Signal) -> Element { }, NavigationItem { route: Route::CategoryDonePage, - icon: FaCheck + icon: FaVolcano }, NavigationItem { route: Route::CategorySomedayMaybePage, - icon: FaQuestion + icon: FaLightbulb }, NavigationItem { route: Route::CategoryWaitingForPage, diff --git a/src/components/navigation_item.rs b/src/components/navigation_item.rs index f14674e..720c535 100644 --- a/src/components/navigation_item.rs +++ b/src/components/navigation_item.rs @@ -13,7 +13,7 @@ pub(crate) fn NavigationItem( Link { to: route.clone(), class: format!( - "py-2.5 flex flex-row justify-center items-center", + "py-2.5 flex flex-row justify-center items-center hover:*:bg-gray-900 active:*:text-gray-400", ), div { class: format!("pt-2.5 px-4 {} transition-all duration-150", diff --git a/src/components/project_form.rs b/src/components/project_form.rs index 07a2206..a3a3cfa 100644 --- a/src/components/project_form.rs +++ b/src/components/project_form.rs @@ -1,82 +1,113 @@ +use crate::components::button_primary::ButtonPrimary; +use crate::components::button_secondary::ButtonSecondary; +use crate::components::input::Input; +use crate::components::input_label::InputLabel; use crate::models::project::Project; use crate::server::projects::{create_project, delete_project, edit_project}; use dioxus::core_macro::{component, rsx}; use dioxus::dioxus_core::Element; use dioxus::prelude::*; use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::fa_solid_icons::{FaFloppyDisk, FaPenClip, FaTrashCan}; +use dioxus_free_icons::icons::fa_solid_icons::{FaFeatherPointed, FaStamp, FaTrashCan, FaXmark}; + +pub(crate) static PROJECT_BEING_EDITED: GlobalSignal> = Signal::global(|| None); #[component] -pub(crate) fn ProjectForm( - project: Option, - on_successful_submit: EventHandler<()>, -) -> Element { +pub(crate) fn ProjectForm() -> Element { + let navigator = use_navigator(); + let project = PROJECT_BEING_EDITED(); let project_for_submit = project.clone(); rsx! { form { + class: "px-4 flex flex-col gap-4", onsubmit: move |event| { event.prevent_default(); let project = project_for_submit.clone(); async move { let new_project = event.parsed_values().unwrap(); - if let Some(project) = project { - let _ = edit_project(project.id, new_project).await; + let result = if let Some(project) = project { + edit_project(project.id, new_project).await } else { - let _ = create_project(new_project).await; + create_project(new_project).await + }; + if result.is_ok() { + navigator.go_back(); } - on_successful_submit.call(()); } }, - class: "p-4 flex flex-col gap-4", + id: "form_project", div { class: "flex flex-row items-center gap-3", - label { - r#for: "input_title", - class: "flex flex-row justify-center items-center min-w-6", - Icon { - class: "text-zinc-400/50", - icon: FaPenClip, - height: 16, - width: 16 - } + InputLabel { + icon: FaFeatherPointed, + r#for: "input_title" } - input { + Input { + class: "grow", name: "title", required: true, - initial_value: project.as_ref().map(|project| project.title.to_owned()), r#type: "text", - class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", - id: "input_title" + initial_value: project.as_ref().map(|project| project.title.to_owned()), } } - div { - class: "flex flex-row justify-between mt-auto", - button { - r#type: "button", - class: "py-3 px-4 bg-zinc-300/50 rounded-lg cursor-pointer", - onclick: move |_| { + } + div { + class: "px-4 grid grid-cols-3 gap-3 mt-auto", + ButtonSecondary { + r#type: "button", + class: "grow", + onclick: { + let project = project.clone(); + move |_| { let project = project.clone(); async move { if let Some(project) = project { - let _ = delete_project(project.id).await; + let result = delete_project(project.id).await; + if result.is_ok() { + /* TODO: Might not work on mobile due to + https://dioxuslabs.com/learn/0.7/essentials/router/navigation#history-buttons. + */ + navigator.go_back(); + } + } else { + navigator.go_back(); + } + } + } + }, + Icon { + icon: FaTrashCan, + height: 16, + width: 16 + } + } + if project.is_some() { + div { + class: "grow flex flex-col items-stretch", + GoBackButton { + ButtonSecondary { + /* TODO: Replace w-full` with proper flexbox styling once + https://github.com/DioxusLabs/dioxus/issues/5269 is solved. */ + class: "w-full", + r#type: "button", + Icon { + icon: FaXmark, + height: 16, + width: 16 } - on_successful_submit.call(()); } - }, - Icon { - icon: FaTrashCan, - height: 16, - width: 16 } } - button { - r#type: "submit", - class: "py-3 px-4 bg-zinc-300/50 rounded-lg cursor-pointer", - Icon { - icon: FaFloppyDisk, - height: 16, - width: 16 - } + } else { + div {} + } + ButtonPrimary { + form: "form_project", + r#type: "submit", + Icon { + icon: FaStamp, + height: 16, + width: 16 } } } diff --git a/src/components/project_list.rs b/src/components/project_list.rs index 28b5ae1..aa8001f 100644 --- a/src/components/project_list.rs +++ b/src/components/project_list.rs @@ -1,24 +1,22 @@ -use crate::{hooks::use_projects, models::project::Project}; +use crate::route::Route; +use crate::{components::project_form::PROJECT_BEING_EDITED, hooks::use_projects}; use dioxus::prelude::*; #[component] pub(crate) fn ProjectList() -> Element { + let navigator = use_navigator(); let projects = use_projects()?; - let mut project_being_edited = use_context::>>(); - rsx! { div { class: "flex flex-col", for project in projects { div { + class: "px-7 py-4 hover:bg-gray-800 font-medium text-pretty wrap-anywhere select-none transition-all duration-150 cursor-pointer", key: "{project.id}", - class: format!( - "px-7 py-4 select-none {} text-pretty wrap-anywhere", - if project_being_edited().is_some_and(|p| p.id == project.id) { - "bg-zinc-700" - } else { "" } - ), - onclick: move |_| project_being_edited.set(Some(project.clone())), + onclick: move |_| { + *PROJECT_BEING_EDITED.write() = Some(project.clone()); + navigator.push(Route::ProjectFormPage); + }, {project.title.clone()} } } diff --git a/src/components/project_select.rs b/src/components/project_select.rs index ff39aaa..a4d05aa 100644 --- a/src/components/project_select.rs +++ b/src/components/project_select.rs @@ -10,8 +10,8 @@ pub(crate) fn ProjectSelect(initial_selected_id: Option) -> Element { rsx! { select { name: "project_id", - class: "px-4 pt-3 pb-2.25 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer", - id: "input_project", + class: "px-4 pt-3 pb-2.25 bg-gray-800-muted enabled:hover:bg-gray-800 enabled:active:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer", + id: "input_project_id", option { value: 0, {t!("none")} diff --git a/src/components/select_button.rs b/src/components/select_button.rs index b858537..50eba12 100644 --- a/src/components/select_button.rs +++ b/src/components/select_button.rs @@ -1,10 +1,5 @@ -use crate::models::project::Project; -use crate::models::task::Task; use dioxus::prelude::*; -use dioxus_free_icons::{ - Icon, IconShape, - icons::fa_solid_icons::{FaGavel, FaPlus, FaXmark}, -}; +use dioxus_free_icons::{Icon, IconShape}; #[component] pub(crate) fn SelectButton( @@ -18,7 +13,7 @@ pub(crate) fn SelectButton( class: format!( "pt-4.5 flex flex-row justify-center items-center {} rounded-xl transition-all duration-150", if is_selected { "pb-3.75 bg-gray-900 drop-shadow-[0_0_0_var(--color-gray-900-muted)]" } - else { "pb-2.75 mt-1 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] text-gray-400 cursor-pointer" } + else { "pb-2.75 mt-1 bg-gray-800-muted hover:bg-gray-800 drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] text-gray-400 cursor-pointer" } ), onclick: move |_| { on_select.call(()); diff --git a/src/components/subtasks_form.rs b/src/components/subtasks_form.rs index fb4948d..1c7f34b 100644 --- a/src/components/subtasks_form.rs +++ b/src/components/subtasks_form.rs @@ -1,3 +1,6 @@ +use crate::components::button_secondary::ButtonSecondary; +use crate::components::input::Input; +use crate::components::input_label::InputLabel; use crate::hooks::use_subtasks_of_task; use crate::models::subtask::NewSubtask; use crate::models::task::Task; @@ -6,146 +9,131 @@ use dioxus::core_macro::{component, rsx}; use dioxus::dioxus_core::Element; use dioxus::prelude::*; use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::fa_regular_icons::FaSquare; -use dioxus_free_icons::icons::fa_solid_icons::{FaListCheck, FaPlus, FaSquareCheck, FaTrashCan}; +use dioxus_free_icons::icons::fa_solid_icons::{FaGavel, FaListCheck, FaTrashCan}; #[component] pub(crate) fn SubtasksForm(task: Task) -> Element { let subtasks = use_subtasks_of_task(task.id)?; let mut new_title = use_signal(String::new); rsx! { - form { - class: "flex flex-row items-center gap-3", - onsubmit: move |event| { - event.prevent_default(); - let task = task.clone(); - async move { - let new_subtask = NewSubtask { - task_id: task.id, - title: event.get("title").first().cloned().and_then(|value| match value { - FormValue::Text(value) => Some(value), - FormValue::File(_) => None - }).unwrap(), - is_completed: false - }; - let _ = create_subtask(new_subtask).await; - new_title.set(String::new()); - } - }, - label { - r#for: "input_new_title", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaListCheck, - height: 16, - width: 16 - } - } - div { - class: "grow grid grid-cols-6 gap-2", - input { - name: "title", - required: true, - value: new_title, - r#type: "text", - class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", - id: "input_new_title", - onchange: move |event| new_title.set(event.value()) - } - button { - r#type: "submit", - class: "py-2 col-span-1 flex flex-row justify-center items-center bg-zinc-800/50 rounded-lg cursor-pointer", - Icon { - icon: FaPlus, - height: 16, - width: 16 - } - } - } - } - for subtask in subtasks { - div { - key: "{subtask.id}", + div { + class: "flex flex-col gap-3", + form { class: "flex flex-row items-center gap-3", - button { - class: "min-w-6 flex flex-row justify-center items-center text-zinc-400/50 cursor-pointer", - onclick: { - let subtask = subtask.clone(); - move |_| { - let subtask = subtask.clone(); - async move { - let new_subtask = NewSubtask { - task_id: subtask.task_id, - title: subtask.title.clone(), - is_completed: !subtask.is_completed - }; - let _ = edit_subtask( - subtask.id, - new_subtask - ).await; - } - } - }, - if subtask.is_completed { - Icon { - icon: FaSquareCheck, - height: 24, - width: 24 - } - } else { - Icon { - icon: FaSquare, - height: 24, - width: 24 - } + onsubmit: move |event| { + event.prevent_default(); + let task = task.clone(); + async move { + let new_subtask = NewSubtask { + task_id: task.id, + title: event.get("new_title").first().cloned().and_then(|value| match value { + FormValue::Text(value) => Some(value), + FormValue::File(_) => None + }).unwrap(), + is_completed: false + }; + let _ = create_subtask(new_subtask).await; + new_title.set(String::new()); } + }, + InputLabel { + icon: FaListCheck, + r#for: "input_new_title" } div { - class: "grow grid grid-cols-6 gap-2", - input { + class: "grow flex flex-row items-end gap-3", + Input { + class: "grow", + name: "new_title", r#type: "text", - class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", - id: "input_title_{subtask.id}", - initial_value: subtask.title.clone(), - onchange: { - let subtask = subtask.clone(); - move |event: Event| { - let subtask = subtask.clone(); - async move { - let new_subtask = NewSubtask { - task_id: subtask.task_id, - title: event.value(), - is_completed: subtask.is_completed - }; - if new_subtask.title.is_empty() { - let _ = delete_subtask(subtask.id).await; - } else { - let _ = edit_subtask( - subtask.id, - new_subtask - ).await; - } - } - } + required: true, + value: new_title, + onchange: move |event: Event| new_title.set(event.value()) + } + ButtonSecondary { + r#type: "submit", + Icon { + icon: FaGavel, + height: 16, + width: 16 } } + } + } + for subtask in subtasks { + div { + key: "{subtask.id}", + class: "flex flex-row items-center gap-3", button { - r#type: "button", - class: "py-2 flex flex-row justify-center items-center col-span-1 bg-zinc-800/50 rounded-lg cursor-pointer", + class: "mt-1.5 hover:mt-1 hover:pb-0.5 min-w-7 cursor-pointer transition-all duration-150", onclick: { let subtask = subtask.clone(); move |_| { let subtask = subtask.clone(); async move { - let _ = delete_subtask(subtask.id).await; + let new_subtask = NewSubtask { + task_id: subtask.task_id, + title: subtask.title.clone(), + is_completed: !subtask.is_completed + }; + let _ = edit_subtask( + subtask.id, + new_subtask + ).await; } } }, - Icon { - icon: FaTrashCan, - height: 16, - width: 16 + div { + class: format!("grow h-7 w-7 mb-[4px] drop-shadow-[0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800),0_1px_0_var(--color-gray-800)] rounded-full {}", + if subtask.is_completed {"bg-gray-600"} else {"border-3 border-gray-600"} + ) + } + } + div { + class: "grow flex flex-row items-end gap-3", + Input { + class: "grow", + name: "title_edit_{subtask.id}", + r#type: "text", + initial_value: subtask.title.clone(), + onchange: { + let subtask = subtask.clone(); + move |event: Event| { + let subtask = subtask.clone(); + async move { + let new_subtask = NewSubtask { + task_id: subtask.task_id, + title: event.value(), + is_completed: subtask.is_completed + }; + if new_subtask.title.is_empty() { + let _ = delete_subtask(subtask.id).await; + } else { + let _ = edit_subtask( + subtask.id, + new_subtask + ).await; + } + } + } + } + } + ButtonSecondary { + r#type: "button", + onclick: { + let subtask = subtask.clone(); + move |_| { + let subtask = subtask.clone(); + async move { + let _ = delete_subtask(subtask.id).await; + } + } + }, + Icon { + icon: FaTrashCan, + height: 16, + width: 16 + } } } } diff --git a/src/components/task_form.rs b/src/components/task_form.rs index 3de6c92..3f83e63 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -1,3 +1,5 @@ +use crate::components::button_primary::ButtonPrimary; +use crate::components::button_secondary::ButtonSecondary; use crate::components::category_input::CategoryInput; use crate::components::input::Input; use crate::components::input_label::InputLabel; @@ -7,7 +9,6 @@ use crate::components::subtasks_form::SubtasksForm; use crate::models::category::{CalendarTime, Category, Reoccurrence}; use crate::models::task::NewTask; use crate::models::task::Task; -use crate::route::Route; use crate::server::tasks::{create_task, delete_task, edit_task}; use chrono::Duration; use dioxus::core_macro::{component, rsx}; @@ -15,8 +16,8 @@ use dioxus::dioxus_core::Element; use dioxus::prelude::*; use dioxus_free_icons::Icon; use dioxus_free_icons::icons::fa_solid_icons::{ - FaBell, FaBomb, FaClock, FaFeatherPointed, FaFloppyDisk, FaHourglassEnd, FaLayerGroup, FaList, - FaPenClip, FaRepeat, FaScroll, FaTrashCan, FaVolcano, FaXmark, + FaBell, FaBomb, FaClock, FaFeatherPointed, FaHourglassEnd, FaList, FaRepeat, FaScroll, FaStamp, + FaTrashCan, FaXmark, }; use dioxus_i18n::t; use serde::{Deserialize, Serialize}; @@ -53,24 +54,19 @@ struct InputData { project_id: Option, } +pub(crate) static TASK_BEING_EDITED: GlobalSignal> = Signal::global(|| None); +pub(crate) static LATEST_VISITED_CATEGORY: GlobalSignal = + Signal::global(|| Category::Inbox); + #[component] -pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<()>) -> Element { - let route = use_route::(); +pub(crate) fn TaskForm() -> Element { + let navigator = use_navigator(); + let task = TASK_BEING_EDITED(); let selected_category = use_signal(|| { if let Some(task) = &task { task.category.clone() } else { - match route { - Route::CategorySomedayMaybePage => Category::SomedayMaybe, - Route::CategoryWaitingForPage => Category::WaitingFor(String::new()), - Route::CategoryNextStepsPage => Category::NextSteps, - Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar { - date: chrono::Local::now().date_naive(), - reoccurrence: None, - time: None, - }, - _ => Category::Inbox, - } + LATEST_VISITED_CATEGORY() } }); let category_calendar_reoccurrence_interval = use_signal(|| { @@ -110,7 +106,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() rsx! { div { - class: "p-4 flex flex-col gap-10", + class: "grow px-4 flex flex-col gap-6.5", form { class: "flex flex-col gap-8", id: "form_task", @@ -154,12 +150,14 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() project_id: input_data.project_id .and_then(|deadline| deadline.parse().ok()).filter(|&id| id > 0), }; - if let Some(task) = task { - let _ = edit_task(task.id, new_task).await; + let result = if let Some(task) = task { + edit_task(task.id, new_task).await } else { - let _ = create_task(new_task).await; + create_task(new_task).await + }; + if result.is_ok() { + navigator.go_back(); } - on_successful_submit.call(()); } }, div { @@ -169,6 +167,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() icon: FaFeatherPointed }, Input { + class: "grow", name: "title", required: true, initial_value: task.as_ref().map(|task| task.title.clone()), @@ -179,17 +178,16 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "input_project", + r#for: "input_project_id", icon: FaList }, SuspenseBoundary { fallback: |_| { rsx ! { select { - class: "px-4.5 pt-3.5 pb-2.75 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer", + class: "px-4 pt-3 pb-2.25 bg-gray-800-muted drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-900-muted)] rounded-xl grow cursor-pointer", option { - value: 0, - {t!("none")} + value: 0 } } } @@ -202,25 +200,25 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "input_deadline", - icon: FaBomb + icon: FaBomb, + r#for: "input_deadline" }, Input { name: "deadline", initial_value: task.as_ref().and_then(|task| task.deadline) .map(|deadline| deadline.format("%Y-%m-%d").to_string()), r#type: "date", - class: "grow basis-0 cursor-pointer" + class: "grow basis-0" } }, div { class: "flex flex-row items-center gap-3", InputLabel { - icon: FaScroll, + icon: FaScroll }, CategoryInput { - selected_category: selected_category, - class: "grow" + class: "grow", + selected_category: selected_category } } match selected_category() { @@ -228,10 +226,11 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "input_category_waiting_for", icon: FaHourglassEnd, + r#for: "input_category_waiting_for", }, Input { + class: "grow", name: "category_waiting_for", required: true, initial_value: waiting_for, @@ -243,20 +242,22 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "input_category_calendar_date", - icon: FaClock + icon: FaClock, + r#for: "input_category_calendar_date" }, div { class: "grow flex flex-row gap-3", Input { - r#type: "date", + class: "grow", name: "category_calendar_date", + r#type: "date", required: true, initial_value: date.format("%Y-%m-%d").to_string(), }, Input { - r#type: "time", + class: "grow", name: "category_calendar_time", + r#type: "time", initial_value: time.map(|calendar_time| calendar_time.time.format("%H:%M").to_string() ), @@ -269,8 +270,8 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "category_calendar_reoccurrence_length", - icon: FaRepeat + icon: FaRepeat, + r#for: "category_calendar_reoccurrence_length" }, div { class: "grow grid grid-cols-5 items-end gap-3", @@ -278,6 +279,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() reoccurrence_interval: category_calendar_reoccurrence_interval }, Input { + class: "text-right", r#type: "number", inputmode: "numeric", name: "category_calendar_reoccurrence_length", @@ -288,7 +290,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() String::new(), |_| reoccurrence.map_or(1, |reoccurrence| reoccurrence.length).to_string() - ), + ) } } }, @@ -296,18 +298,17 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() div { class: "flex flex-row items-center gap-3", InputLabel { - r#for: "category_calendar_reminder_offset_index", + r#for: "input_category_calendar_reminder_offset_index", icon: FaBell }, input { - r#type: "range", + class: "grow", name: "category_calendar_reminder_offset_index", + r#type: "range", min: 0, max: REMINDER_OFFSETS.len() as i64 - 1, initial_value: category_calendar_reminder_offset_index() .to_string(), - class: "grow cursor-pointer", - id: "category_calendar_has_reminder", oninput: move |event| { category_calendar_reminder_offset_index.set( event.value().parse().unwrap() @@ -315,8 +316,8 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() } }, label { - r#for: "category_calendar_reminder_offset_index", class: "pr-3 min-w-16 text-right", + r#for: "category_calendar_reminder_offset_index", {REMINDER_OFFSETS[category_calendar_reminder_offset_index()] .map( |offset| if offset.num_hours() < 1 { @@ -342,17 +343,20 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() } } } - div { - class: "flex flex-row gap-3 mt-auto", - button { - r#type: "button", - class: "grow py-3 px-4 flex flex-row justify-center items-center bg-gray-600 drop-shadow-[0_6px_0_var(--color-gray-800)] rounded-xl cursor-pointer", - onclick: move |_| { + } + div { + class: "px-4 grid grid-cols-3 gap-3 mt-auto", + ButtonSecondary { + r#type: "button", + class: "grow", + onclick: { + let task = task.clone(); + move |_| { let task = task.clone(); async move { if let Some(task) = task { - if let Category::Trash = task.category { - let _ = delete_task(task.id).await; + let result = if let Category::Trash = task.category { + delete_task(task.id).await } else { let new_task = NewTask { title: task.title.to_owned(), @@ -360,37 +364,53 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() category: Category::Trash, project_id: task.project_id }; - let _ = edit_task(task.id, new_task).await; + edit_task(task.id, new_task).await.map(|_| ()) + }; + if result.is_ok() { + /* TODO: Might not work on mobile due to + https://dioxuslabs.com/learn/0.7/essentials/router/navigation#history-buttons. + */ + navigator.go_back(); } + } else { + navigator.go_back(); + } + } + } + }, + Icon { + icon: FaTrashCan, + height: 16, + width: 16 + } + } + if task.is_some() { + div { + class: "grow flex flex-col items-stretch", + GoBackButton { + ButtonSecondary { + /* TODO: Replace w-full` with proper flexbox styling once + https://github.com/DioxusLabs/dioxus/issues/5269 is solved. */ + class: "w-full", + r#type: "button", + Icon { + icon: FaXmark, + height: 16, + width: 16 } - on_successful_submit.call(()); } - }, - Icon { - icon: FaVolcano, - height: 16, - width: 16 } } - button { - r#type: "button", - class: "grow py-3 px-4 flex flex-row justify-center items-center bg-gray-600 drop-shadow-[0_6px_0_var(--color-gray-800)] rounded-xl cursor-pointer", - onclick: move |_| {}, - Icon { - icon: FaXmark, - height: 16, - width: 16 - } - } - button { - form: "form_task", - r#type: "submit", - class: "grow py-3.5 px-4 flex flex-row justify-center items-center bg-amber-300-muted drop-shadow-[0_6px_0_var(--color-amber-700-muted)] text-amber-700-muted rounded-xl cursor-pointer", - Icon { - icon: FaFloppyDisk, - height: 16, - width: 16 - } + } else { + div {} + } + ButtonPrimary { + form: "form_task", + r#type: "submit", + Icon { + icon: FaStamp, + height: 16, + width: 16 } } } diff --git a/src/components/task_list.rs b/src/components/task_list.rs index c13233d..1d1bc8d 100644 --- a/src/components/task_list.rs +++ b/src/components/task_list.rs @@ -1,17 +1,16 @@ +use crate::components::task_form::TASK_BEING_EDITED; use crate::components::task_list_item::TaskListItem; use crate::models::category::Category; -use crate::models::task::{Task, TaskWithSubtasks}; +use crate::models::task::TaskWithSubtasks; +use crate::route::Route; use crate::server::tasks::complete_task; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::fa_regular_icons::{FaCircle, FaSquare}; -use dioxus_free_icons::icons::fa_solid_icons::FaSquareCheck; #[component] pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element { - let mut task_being_edited = use_context::>>(); + let navigator = use_navigator(); rsx! { div { class: format!("flex flex-col {}", class.unwrap_or("")), @@ -19,27 +18,32 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str> div { key: "{task.task.id}", class: format!( - "px-7 pt-4.25 {} flex flex-row items-start gap-4 select-none {}", + "px-7 pt-3.75 {} flex flex-row items-start gap-4 hover:bg-gray-800 cursor-pointer select-none transition-all duration-150", if task.task.deadline.is_some() || !task.subtasks.is_empty() { "pb-0.25" } else if let Category::Calendar { time, .. } = &task.task.category { if time.is_some() { "pb-0.25" } else { - "pb-4.25" + "pb-3.75" } } else { - "pb-4.25" + "pb-3.75" }, - if task_being_edited().is_some_and(|t| t.id == task.task.id) { - "bg-zinc-700" - } else { "" } ), onclick: { let task = task.clone(); - move |_| task_being_edited.set(Some(task.task.clone())) + move |_| { + *TASK_BEING_EDITED.write() = Some(task.task.clone()); + navigator.push(Route::TaskFormPage); + } }, button { + class: format!( + "mt-0.5 hover:mt-0 hover:pb-0.5 transition-all duration-150 cursor-pointer {}", + if let Category::Done = task.task.category { "pointer-events-none" } + else { "" } + ), onclick: { move |event: Event| { // To prevent editing the task. @@ -49,20 +53,18 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str> } } }, - if let Category::Done = task.task.category { - Icon { - icon: FaSquareCheck, - height: 30, - width: 30 - } - } else { - div { - class: "h-8 w-8 border-3 border-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)] rounded-full cursor-pointer" - } + div { + class: format!("h-8 w-8 rounded-full {}", + if let Category::Done = task.task.category { + "mt-[3px] mb-[2px] bg-amber-300-muted" + } else { + "mb-[5px] border-3 border-amber-300-muted drop-shadow-[0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted),0_1px_0_var(--color-amber-700-muted)]" + } + ) } }, div { - class: "mt-1", + class: "mt-1.5", TaskListItem { task: task.clone() } diff --git a/src/components/task_list_item.rs b/src/components/task_list_item.rs index 26b3b47..dfd205b 100644 --- a/src/components/task_list_item.rs +++ b/src/components/task_list_item.rs @@ -25,7 +25,7 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element { class: "flex flex-row gap-4", if let Some(deadline) = task.task.deadline { div { - class: "flex flex-row items-center gap-1 text-sm text-zinc-400", + class: "flex flex-row items-center gap-1 text-sm text-gray-500", Icon { icon: FaBomb, height: 14, @@ -76,7 +76,7 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element { if let Category::Calendar { time, .. } = task.task.category { if let Some(calendar_time) = time { div { - class: "flex flex-row items-center gap-1 text-sm text-zinc-400", + class: "flex flex-row items-center gap-1 text-sm text-gray-500", Icon { icon: FaClock, height: 14, @@ -91,7 +91,7 @@ pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element { } if !task.subtasks.is_empty() { div { - class: "flex flex-row items-center gap-1 text-sm text-zinc-400", + class: "flex flex-row items-center gap-1 text-sm text-gray-500", Icon { icon: FaListCheck, height: 14, diff --git a/src/layouts/main.rs b/src/layouts/main.rs deleted file mode 100644 index ac6e7cb..0000000 --- a/src/layouts/main.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::components::bottom_panel::BottomPanel; -use crate::components::form_open_button::FormOpenButton; -use crate::components::sticky_bottom::StickyBottom; -use crate::models::project::Project; -use crate::models::task::Task; -use crate::route::Route; -use dioxus::core_macro::rsx; -use dioxus::dioxus_core::Element; -use dioxus::prelude::*; -use dioxus_free_icons::Icon; -use dioxus_free_icons::icons::fa_solid_icons::FaCog; - -#[component] -pub(crate) fn Main() -> Element { - let mut display_form = use_signal(|| false); - let project_being_edited = - use_context_provider::>>(|| Signal::new(None)); - let task_being_edited = use_context_provider::>>(|| Signal::new(None)); - - use_effect(move || { - display_form.set(project_being_edited().is_some() || task_being_edited().is_some()); - }); - - rsx! { - SuspenseBoundary { - fallback: |_| { - rsx! { - div { - class: "grow flex flex-col justify-center items-center", - Icon { - class: "text-gray-500 animate-[spin_2000ms_linear_infinite]", - icon: FaCog, - height: 32, - width: 32 - } - } - } - }, - Outlet:: {} - } - StickyBottom { - FormOpenButton { - is_opened: display_form, - } - BottomPanel { - display_form: display_form, - } - } - } -} diff --git a/src/layouts/mod.rs b/src/layouts/mod.rs index 052501d..896ab04 100644 --- a/src/layouts/mod.rs +++ b/src/layouts/mod.rs @@ -1,2 +1,2 @@ -mod main; -pub(crate) use main::Main; +pub(crate) mod navigation; +pub(crate) mod suspense; diff --git a/src/layouts/navigation.rs b/src/layouts/navigation.rs new file mode 100644 index 0000000..9dc2b5f --- /dev/null +++ b/src/layouts/navigation.rs @@ -0,0 +1,38 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::create_button::CreateButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::LATEST_VISITED_CATEGORY; +use crate::models::category::Category; +use crate::route::Route; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; + +#[component] +pub(crate) fn Navigation() -> Element { + let current_route = use_route(); + use_effect(use_reactive(¤t_route, move |current_route| { + *LATEST_VISITED_CATEGORY.write() = match current_route { + Route::CategorySomedayMaybePage => Category::SomedayMaybe, + Route::CategoryWaitingForPage => Category::WaitingFor(String::new()), + Route::CategoryNextStepsPage => Category::NextSteps, + Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar { + date: chrono::Local::now().date_naive(), + reoccurrence: None, + time: None, + }, + _ => Category::Inbox, + }; + })); + + rsx! { + div { + class: "grow flex flex-col pb-36", + Outlet:: {} + } + StickyBottom { + CreateButton {}, + BottomPanel {} + } + } +} diff --git a/src/layouts/suspense.rs b/src/layouts/suspense.rs new file mode 100644 index 0000000..1e7f0e8 --- /dev/null +++ b/src/layouts/suspense.rs @@ -0,0 +1,28 @@ +use crate::route::Route; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use dioxus_free_icons::Icon; +use dioxus_free_icons::icons::fa_solid_icons::FaCog; + +#[component] +pub(crate) fn Suspense() -> Element { + rsx! { + SuspenseBoundary { + fallback: |_| { + rsx! { + div { + class: "grow flex flex-col justify-center items-center", + Icon { + class: "text-gray-500 animate-[spin_3000ms_linear_infinite]", + icon: FaCog, + height: 32, + width: 32 + } + } + } + }, + Outlet:: {} + } + } +} diff --git a/src/route/mod.rs b/src/route/mod.rs index a25b856..c573cb4 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -8,7 +8,9 @@ use crate::views::category_today_page::CategoryTodayPage; use crate::views::category_trash_page::CategoryTrashPage; use crate::views::category_waiting_for_page::CategoryWaitingForPage; use crate::views::not_found_page::NotFoundPage; +use crate::views::project_form_page::ProjectFormPage; use crate::views::projects_page::ProjectsPage; +use crate::views::task_form_page::TaskFormPage; use dioxus::prelude::*; // All variants have the same postfix because they have to match the component names. @@ -16,26 +18,33 @@ use dioxus::prelude::*; #[derive(Clone, Routable, Debug, PartialEq)] #[rustfmt::skip] pub(crate) enum Route { - #[layout(layouts::Main)] - #[redirect("/", || Route::CategoryTodayPage {})] - #[route("/today")] - CategoryTodayPage, - #[route("/inbox")] - CategoryInboxPage, - #[route("/someday-maybe")] - CategorySomedayMaybePage, - #[route("/waiting-for")] - CategoryWaitingForPage, - #[route("/next-steps")] - CategoryNextStepsPage, - #[route("/calendar")] - CategoryCalendarPage, - #[route("/done")] - CategoryDonePage, - #[route("/trash")] - CategoryTrashPage, - #[route("/projects")] - ProjectsPage, + #[layout(layouts::navigation::Navigation)] + #[layout(layouts::suspense::Suspense)] + #[route("/today")] + CategoryTodayPage, + #[route("/inbox")] + CategoryInboxPage, + #[route("/someday-maybe")] + CategorySomedayMaybePage, + #[route("/waiting-for")] + CategoryWaitingForPage, + #[route("/next-steps")] + CategoryNextStepsPage, + #[route("/calendar")] + CategoryCalendarPage, + #[route("/done")] + CategoryDonePage, + #[route("/trash")] + CategoryTrashPage, + #[route("/projects")] + ProjectsPage, + #[end_layout] + #[end_layout] + #[layout(layouts::suspense::Suspense)] + #[route("/task")] + TaskFormPage, + #[route("/project")] + ProjectFormPage, #[end_layout] #[redirect("/", || Route::CategoryTodayPage)] #[route("/:..route")] diff --git a/src/views/category_inbox_page.rs b/src/views/category_inbox_page.rs index a14ac79..643ed6d 100644 --- a/src/views/category_inbox_page.rs +++ b/src/views/category_inbox_page.rs @@ -1,5 +1,4 @@ use crate::components::error_boundary_message::ErrorBoundaryMessage; -use crate::components::task_form::TaskForm; use crate::models::category::Category; use crate::views::category_page::CategoryPage; use dioxus::core_macro::rsx; @@ -13,10 +12,6 @@ pub(crate) fn CategoryInboxPage() -> Element { CategoryPage { category: Category::Inbox, } - TaskForm { - task: None, - on_successful_submit: move |_| {} - } } } } diff --git a/src/views/category_page.rs b/src/views/category_page.rs index 64fb8e3..b069551 100644 --- a/src/views/category_page.rs +++ b/src/views/category_page.rs @@ -11,8 +11,7 @@ pub(crate) fn CategoryPage(category: Category) -> Element { rsx! { TaskList { - tasks: tasks.clone(), - class: "pb-36" + tasks: tasks.clone() } } } diff --git a/src/views/mod.rs b/src/views/mod.rs index 892573d..e19c82b 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -8,4 +8,6 @@ pub(crate) mod category_today_page; pub(crate) mod category_trash_page; pub(crate) mod category_waiting_for_page; pub(crate) mod not_found_page; +pub(crate) mod project_form_page; pub(crate) mod projects_page; +pub(crate) mod task_form_page; diff --git a/src/views/project_form_page.rs b/src/views/project_form_page.rs new file mode 100644 index 0000000..7528841 --- /dev/null +++ b/src/views/project_form_page.rs @@ -0,0 +1,12 @@ +use crate::components::{error_boundary_message::ErrorBoundaryMessage, project_form::ProjectForm}; +use dioxus::prelude::*; + +#[component] +pub(crate) fn ProjectFormPage() -> Element { + rsx! { + ErrorBoundaryMessage { + class: "grow py-4 flex flex-col gap-12", + ProjectForm {} + } + } +} diff --git a/src/views/task_form_page.rs b/src/views/task_form_page.rs new file mode 100644 index 0000000..c184611 --- /dev/null +++ b/src/views/task_form_page.rs @@ -0,0 +1,12 @@ +use crate::components::{error_boundary_message::ErrorBoundaryMessage, task_form::TaskForm}; +use dioxus::prelude::*; + +#[component] +pub(crate) fn TaskFormPage() -> Element { + rsx! { + ErrorBoundaryMessage { + class: "grow py-4 flex flex-col gap-12", + TaskForm {} + } + } +}