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/manifest.json b/assets/manifest.json index ee671f6..a0062c6 100644 --- a/assets/manifest.json +++ b/assets/manifest.json @@ -3,8 +3,8 @@ "short_name": "Todo Baggins", "start_url": "/", "display": "standalone", - "background_color": "#27272a", - "theme_color": "#27272a", + "background_color": "#101828", + "theme_color": "#b89a2e", "icons": [ { "src": "/assets/images/icon.png", diff --git a/assets/styles/input_range.css b/assets/styles/input_range.css index 555e054..b48083c 100644 --- a/assets/styles/input_range.css +++ b/assets/styles/input_range.css @@ -8,56 +8,37 @@ input[type="range"] { background: transparent; } -input[type="range"]::-moz-range-thumb { +input[type="range"]::-moz-range-thumb, +input[type="range"]::-webkit-slider-thumb { width: 1.25rem; height: 1.25rem; - background: rgba(228 228 231); + background: var(--color-gray-400); + filter: drop-shadow(0 var(--spacing) 0 var(--color-gray-500)); border: 0; border-radius: 0.5rem; + cursor: pointer; } -input[type="range"]::-moz-range-progress { - background: #525259; +input[type="range"]::-webkit-slider-thumb { + position: relative; + top: -9px; +} + +input[type="range"]::-moz-range-track, +input[type="range"]::-webkit-slider-runnable-track { + background: var(--color-gray-800-muted); height: 0.5rem; + filter: drop-shadow( + 0 calc(0px - var(--spacing)) 0 var(--color-gray-900-muted) + ); border-radius: 0.25rem; } input[type="range"]::-moz-range-track { - background: rgba(39 39 42 / 50%); - height: 0.5rem; - border-radius: 0.25rem; -} - -input[type="range"].input-range-reverse::-moz-range-progress { - background: #2d2d31; - height: 0.5rem; - border-radius: 0.25rem; -} - -input[type="range"].input-range-reverse::-moz-range-track { - background: rgba(113 113 122 / 50%); - height: 0.5rem; - border-radius: 0.25rem; -} - -input[type="range"]::-webkit-slider-thumb { - width: 1.25rem; - height: 1.25rem; - background: rgba(228 228 231); - border: 0; - border-radius: 0.5rem; - position: relative; - top: -0.4rem; + transform: translateY(3px); } input[type="range"]::-webkit-slider-runnable-track { - background: rgba(39 39 42 / 50%); - height: 0.5rem; - border-radius: 0.25rem; -} - -input[type="range"].input-range-reverse::-webkit-slider-runnable-track { - background: rgba(39 39 42 / 50%); - height: 0.5rem; - border-radius: 0.25rem; + position: relative; + top: 3px; } diff --git a/assets/styles/select_arrow.css b/assets/styles/select_arrow.css new file mode 100644 index 0000000..585e826 --- /dev/null +++ b/assets/styles/select_arrow.css @@ -0,0 +1,7 @@ +select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'%3E%3Cpath fill='%239ca3af' d='M300.3 440.8C312.9 451 331.4 450.3 343.1 438.6L471.1 310.6C480.3 301.4 483 287.7 478 275.7C473 263.7 461.4 256 448.5 256L192.5 256C179.6 256 167.9 263.8 162.9 275.8C157.9 287.8 160.7 301.5 169.9 310.6L297.9 438.6L300.3 440.8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-size: 2rem; + background-position: right 0.5rem center; +} diff --git a/src/components/app.rs b/src/components/app.rs index 553ee51..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; @@ -15,6 +16,7 @@ static FONTS_DIRECTORY: Asset = asset!( const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); const INPUT_NUMBER_ARROWS_CSS: Asset = asset!("/assets/styles/input_number_arrows.css"); const INPUT_RANGE_CSS: Asset = asset!("/assets/styles/input_range.css"); +const SELECT_ARROW_CSS: Asset = asset!("/assets/styles/select_arrow.css"); const MANIFEST: Asset = asset!("/assets/manifest.json"); #[component] @@ -36,10 +38,11 @@ pub(crate) fn App() -> Element { document::Stylesheet { href: TAILWIND_CSS } document::Stylesheet { href: INPUT_NUMBER_ARROWS_CSS } document::Stylesheet { href: INPUT_RANGE_CSS } + document::Stylesheet { href: SELECT_ARROW_CSS } document::Link { rel: "manifest", href: MANIFEST, crossorigin: "use-credentials" } div { - class: "min-h-screen pt-4 pb-36 flex flex-col text-zinc-200 bg-zinc-800", + 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 7738415..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-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] 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-[64px]", - (false, _, true) => "h-[130px]", - (true, Route::ProjectsPage, _) => "h-[130px]", - (true, _, _) => "h-[506px]", + "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 {}", + 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 { - 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 b86f805..9420271 100644 --- a/src/components/category_input.rs +++ b/src/components/category_input.rs @@ -1,10 +1,11 @@ +use crate::components::select_button::SelectButton; 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, FaWater, + FaCalendarDays, FaHourglassHalf, FaInbox, FaSignsPost, FaWater, }; #[component] @@ -14,105 +15,51 @@ pub(crate) fn CategoryInput( ) -> Element { rsx! { div { - class: format!("flex flex-row gap-2 {}", class.unwrap_or("")), - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if selected_category() == Category::SomedayMaybe { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + class: format!("grid grid-cols-3 gap-3 {}", class.unwrap_or("")), + SelectButton { + icon: FaLightbulb, + is_selected: matches!(selected_category(), Category::SomedayMaybe), + on_select: move |_| { selected_category.set(Category::SomedayMaybe); - }, - Icon { - icon: FaQuestion, - height: 16, - width: 16 } - }, - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if selected_category() == Category::LongTerm { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + } + SelectButton { + icon: FaWater, + is_selected: matches!(selected_category(), Category::LongTerm), + on_select: move |_| { selected_category.set(Category::LongTerm); - }, - Icon { - icon: FaWater, - height: 16, - width: 16 } - }, - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if let Category::WaitingFor(_) = selected_category() { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + } + SelectButton { + icon: FaHourglassHalf, + is_selected: matches!(selected_category(), Category::WaitingFor(_)), + on_select: move |_| { selected_category.set(Category::WaitingFor(String::new())); - }, - Icon { - icon: FaHourglassHalf, - height: 16, - width: 16 } - }, - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if selected_category() == Category::NextSteps { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + } + SelectButton { + icon: FaSignsPost, + is_selected: matches!(selected_category(), Category::NextSteps), + on_select: move |_| { selected_category.set(Category::NextSteps); - }, - Icon { - icon: FaForward, - height: 16, - width: 16 } - }, - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if let Category::Calendar { .. } = selected_category() { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + } + SelectButton { + icon: FaCalendarDays, + is_selected: matches!(selected_category(), Category::Calendar { .. }), + on_select: move |_| { selected_category.set(Category::Calendar { date: chrono::Local::now().date_naive(), reoccurrence: None, time: None, }); - }, - Icon { - icon: FaCalendarDays, - height: 16, - width: 16 } - }, - button { - r#type: "button", - class: format!( - "py-3 flex flex-row justify-center items-center rounded-lg grow basis-0 {} cursor-pointer", - if selected_category() == Category::Inbox { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { + } + SelectButton { + icon: FaInbox, + is_selected: matches!(selected_category(), Category::Inbox), + on_select: move |_| { selected_category.set(Category::Inbox); - }, - Icon { - icon: FaInbox, - height: 16, - width: 16 } } } diff --git a/src/components/category_today_task_list.rs b/src/components/category_today_task_list.rs index 0cb6e0e..772847c 100644 --- a/src/components/category_today_task_list.rs +++ b/src/components/category_today_task_list.rs @@ -46,29 +46,31 @@ pub(crate) fn CategoryTodayTaskList() -> Element { rsx! { div { class: "pt-4 flex flex-col gap-8", - div { - class: "flex flex-col gap-4", + if !long_term_tasks.is_empty() { div { - class: "px-7 flex flex-row items-center gap-2 font-bold", - Icon { - class: "mx-1", - icon: FaWater - } + class: "flex flex-col gap-4", div { - {t!("long-term")._upper_first()} + class: "px-7 flex flex-row items-center gap-2 text-gray-500 font-bold", + Icon { + class: "mx-1.5", + icon: FaWater + } + div { + {t!("long-term")._upper_first()} + } + } + TaskList { + tasks: long_term_tasks } - } - TaskList { - tasks: long_term_tasks } } if !overdue_tasks.is_empty() { 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", Icon { - class: "mx-1", + class: "mx-1.25", height: 22, width: 22, icon: FaCalendarXmark @@ -86,9 +88,9 @@ pub(crate) fn CategoryTodayTaskList() -> 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", Icon { - class: "mx-1", + class: "mx-1.25", height: 22, width: 22, icon: FaCalendarCheck 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/error_boundary_message.rs b/src/components/error_boundary_message.rs index 4beb4d4..6beb3ac 100644 --- a/src/components/error_boundary_message.rs +++ b/src/components/error_boundary_message.rs @@ -14,6 +14,7 @@ pub(crate) fn ErrorBoundaryMessage(children: Element, class: Option) -> class: "grow flex flex-col justify-center items-center", div { Icon { + class: "text-gray-500", icon: FaTriangleExclamation, height: 32, width: 32 diff --git a/src/components/form_open_button.rs b/src/components/form_open_button.rs deleted file mode 100644 index 253e1c4..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::{FaPlus, FaXmark}, -}; - -#[component] -pub(crate) fn FormOpenButton(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 py-4 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200 cursor-pointer", - onclick: move |_| { - if opened() { - project_being_edited.set(None); - task_being_edited.set(None); - } - opened.set(!opened()); - }, - if opened() { - Icon { - icon: FaXmark, - height: 24, - width: 24 - } - } else { - Icon { - icon: FaPlus, - height: 24, - width: 24 - } - } - } - } -} diff --git a/src/components/input.rs b/src/components/input.rs new file mode 100644 index 0000000..4c29f6d --- /dev/null +++ b/src/components/input.rs @@ -0,0 +1,49 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn Input( + class: Option, + name: String, + r#type: String, + 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 { + class: format!( + /* `w-full` is required for the Chromium renderer to allow the input to shrink + properly. */ + "pt-3 pb-2.25 w-full {} 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, + id: id.unwrap_or(format!("input_{}", name)), + autofocus, + 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 new file mode 100644 index 0000000..b1b859d --- /dev/null +++ b/src/components/input_label.rs @@ -0,0 +1,21 @@ +use dioxus::prelude::*; +use dioxus_free_icons::{Icon, IconShape}; + +#[component] +pub(crate) fn InputLabel( + icon: I, + r#for: Option, +) -> Element { + rsx! { + label { + r#for, + class: "mt-0.5 min-w-7 flex flex-row justify-center items-center", + Icon { + class: "text-gray-600", + icon, + height: 16, + width: 16 + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 81d8ae0..4ca3745 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,16 +1,21 @@ 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; pub(crate) mod navigation_item; pub(crate) mod project_form; pub(crate) mod project_list; pub(crate) mod project_select; -pub(crate) mod reoccurrence_input; +pub(crate) mod reoccurrence_interval_input; +pub(crate) mod select_button; pub(crate) mod sticky_bottom; pub(crate) mod subtasks_form; pub(crate) mod task_form; diff --git a/src/components/navigation.rs b/src/components/navigation.rs index 4c01216..0ced100 100644 --- a/src/components/navigation.rs +++ b/src/components/navigation.rs @@ -2,32 +2,37 @@ 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] -pub(crate) fn Navigation(expanded: Signal) -> Element { +pub(crate) fn Navigation(is_expanded: Signal) -> Element { rsx! { div { class: "grid grid-cols-5 justify-stretch", button { class: format!( - "py-5 flex flex-row justify-center items-center {} cursor-pointer", - if expanded() { "text-zinc-200" } - else { "text-zinc-500" } + "py-2 flex flex-row justify-center items-center cursor-pointer", ), - onclick: move |_| expanded.set(!expanded()), - Icon { - icon: FaBars, - height: 24, - width: 24 + onclick: move |_| is_expanded.set(!is_expanded()), + div { + class: format!("pt-2.5 px-4 {} transition-all duration-150", + if is_expanded() { "pb-2 mt-1 bg-gray-900 text-gray-400 rounded-xl drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-950)]" } + else { "pb-3 bg-gray-800 rounded-xl drop-shadow-[0_0_0_var(--color-gray-950)] text-gray-600" } + ), + Icon { + icon: FaBars, + height: 24, + width: 24 + } } }, NavigationItem { route: Route::CategoryNextStepsPage, - icon: FaForward + icon: FaSignsPost }, NavigationItem { route: Route::CategoryCalendarPage, @@ -41,7 +46,7 @@ pub(crate) fn Navigation(expanded: Signal) -> Element { route: Route::CategoryInboxPage, icon: FaInbox }, - {if expanded() { + {if is_expanded() { rsx! { NavigationItem { route: Route::ProjectsPage, @@ -53,11 +58,11 @@ pub(crate) fn Navigation(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 39c139e..720c535 100644 --- a/src/components/navigation_item.rs +++ b/src/components/navigation_item.rs @@ -13,14 +13,18 @@ pub(crate) fn NavigationItem( Link { to: route.clone(), class: format!( - "py-5 flex flex-row justify-center items-center {}", - if current_route == route { "text-zinc-200" } - else { "text-zinc-500" } + "py-2.5 flex flex-row justify-center items-center hover:*:bg-gray-900 active:*:text-gray-400", ), - Icon { - icon, - height: 24, - width: 24 + div { + class: format!("pt-2.5 px-4 {} transition-all duration-150", + if current_route == route { "pb-2 mt-1 bg-gray-900 text-gray-400 rounded-xl drop-shadow-[0_calc(0px_-_var(--spacing))_0_var(--color-gray-950)]" } + else { "pb-3 bg-gray-800 rounded-xl drop-shadow-[0_0_0_var(--color-gray-950)] text-gray-600" } + ), + Icon { + icon, + height: 24, + width: 24 + } } } } 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 fc3021e..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-3.5 py-2.5 bg-zinc-800/50 rounded-lg 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/reoccurrence_input.rs b/src/components/reoccurrence_input.rs deleted file mode 100644 index 9f84b86..0000000 --- a/src/components/reoccurrence_input.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::models::category::ReoccurrenceInterval; -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::{FaBan, FaEarthEurope, FaMoon, FaSun}; - -#[component] -pub(crate) fn ReoccurrenceIntervalInput( - reoccurrence_interval: Signal>, - class_buttons: Option<&'static str>, -) -> Element { - rsx! { - button { - r#type: "button", - class: format!( - "py-2 flex flex-row justify-center items-center rounded-lg {} {} cursor-pointer", - class_buttons.unwrap_or(""), - if reoccurrence_interval().is_none() { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { - reoccurrence_interval.set(None); - }, - Icon { - icon: FaBan, - height: 16, - width: 16 - } - }, - button { - r#type: "button", - class: format!( - "py-2 flex flex-row justify-center items-center rounded-lg {} {} cursor-pointer", - class_buttons.unwrap_or(""), - if let Some(ReoccurrenceInterval::Day) = reoccurrence_interval() - { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { - reoccurrence_interval.set(Some(ReoccurrenceInterval::Day)) - }, - Icon { - icon: FaSun, - height: 16, - width: 16 - } - }, - button { - r#type: "button", - class: format!( - "py-2 flex flex-row justify-center items-center rounded-lg {} {} cursor-pointer", - class_buttons.unwrap_or(""), - if let Some(ReoccurrenceInterval::Month) = reoccurrence_interval() - { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { - reoccurrence_interval.set(Some(ReoccurrenceInterval::Month)) - }, - Icon { - icon: FaMoon, - height: 16, - width: 16 - } - }, - button { - r#type: "button", - class: format!( - "py-2 flex flex-row justify-center items-center rounded-lg {} {} cursor-pointer", - class_buttons.unwrap_or(""), - if let Some(ReoccurrenceInterval::Year) = reoccurrence_interval() - { "bg-zinc-500/50" } - else { "bg-zinc-800/50" } - ), - onclick: move |_| { - reoccurrence_interval.set(Some(ReoccurrenceInterval::Year)) - }, - Icon { - icon: FaEarthEurope, - height: 16, - width: 16 - } - } - } -} diff --git a/src/components/reoccurrence_interval_input.rs b/src/components/reoccurrence_interval_input.rs new file mode 100644 index 0000000..a4b00dd --- /dev/null +++ b/src/components/reoccurrence_interval_input.rs @@ -0,0 +1,44 @@ +use crate::components::select_button::SelectButton; +use crate::models::category::ReoccurrenceInterval; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use dioxus_free_icons::icons::fa_solid_icons::{FaBan, FaEarthEurope, FaMoon, FaSun}; + +#[component] +pub(crate) fn ReoccurrenceIntervalInput( + reoccurrence_interval: Signal>, + class_buttons: Option<&'static str>, +) -> Element { + rsx! { + // TODO: Abstract into SelectButton. Make it sank into the surface by default, like other inputs (abstract those too haha), and rise it up on selection (rationale: it will influence what is on the surface). + SelectButton { + icon: FaBan, + is_selected: reoccurrence_interval().is_none(), + on_select: move |_| { + reoccurrence_interval.set(None); + } + } + SelectButton { + icon: FaSun, + is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Day)), + on_select: move |_| { + reoccurrence_interval.set(Some(ReoccurrenceInterval::Day)) + } + } + SelectButton { + icon: FaMoon, + is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Month)), + on_select: move |_| { + reoccurrence_interval.set(Some(ReoccurrenceInterval::Month)); + } + } + SelectButton { + icon: FaEarthEurope, + is_selected: matches!(reoccurrence_interval(), Some(ReoccurrenceInterval::Year)), + on_select: move |_| { + reoccurrence_interval.set(Some(ReoccurrenceInterval::Year)); + } + } + } +} diff --git a/src/components/select_button.rs b/src/components/select_button.rs new file mode 100644 index 0000000..50eba12 --- /dev/null +++ b/src/components/select_button.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use dioxus_free_icons::{Icon, IconShape}; + +#[component] +pub(crate) fn SelectButton( + icon: I, + is_selected: bool, + on_select: Callback, +) -> Element { + rsx! { + button { + r#type: "button", + 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 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(()); + }, + Icon { + icon, + height: 16, + width: 16 + } + }, + } +} 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 7d85bd3..1fbe4ca 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -1,11 +1,14 @@ +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; use crate::components::project_select::ProjectSelect; -use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; +use crate::components::reoccurrence_interval_input::ReoccurrenceIntervalInput; 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}; @@ -13,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, FaFloppyDisk, FaHourglassEnd, FaLayerGroup, FaList, FaPenClip, - FaRepeat, FaTrashCan, + FaBell, FaBomb, FaClock, FaFeatherPointed, FaHourglassEnd, FaList, FaRepeat, FaScroll, FaStamp, + FaTrashCan, FaXmark, }; use dioxus_i18n::t; use serde::{Deserialize, Serialize}; @@ -51,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(|| { @@ -108,9 +106,9 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() rsx! { div { - class: "p-4 flex flex-col gap-4", + class: "grow px-4 flex flex-col gap-6.5", form { - class: "flex flex-col gap-4", + class: "flex flex-col gap-8", id: "form_task", onsubmit: move |event| { event.prevent_default(); @@ -152,56 +150,45 @@ 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 { class: "flex flex-row items-center gap-3", - label { + InputLabel { r#for: "input_title", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaPenClip, - height: 16, - width: 16 - } + icon: FaFeatherPointed }, - input { + Input { + class: "grow", name: "title", required: true, initial_value: task.as_ref().map(|task| task.title.clone()), r#type: "text", - class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", - id: "input_title" - }, + autofocus: true + } }, div { class: "flex flex-row items-center gap-3", - label { - r#for: "input_project", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaList, - height: 16, - width: 16 - } + InputLabel { + r#for: "input_project_id", + icon: FaList }, SuspenseBoundary { fallback: |_| { rsx ! { select { - class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg 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 + } } } }, @@ -212,98 +199,69 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() }, div { class: "flex flex-row items-center gap-3", - label { - r#for: "input_deadline", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaBomb, - height: 16, - width: 16 - } + InputLabel { + icon: FaBomb, + r#for: "input_deadline" }, - input { + 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: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0 cursor-pointer", - id: "input_deadline" + class: "grow basis-0" } }, div { class: "flex flex-row items-center gap-3", - label { - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaLayerGroup, - height: 16, - width: 16 - } + InputLabel { + icon: FaScroll }, CategoryInput { - selected_category: selected_category, - class: "grow" + class: "grow", + selected_category: selected_category } } match selected_category() { Category::WaitingFor(waiting_for) => rsx! { div { class: "flex flex-row items-center gap-3", - label { - r#for: "input_waiting_for", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaHourglassEnd, - height: 16, - width: 16 - } + InputLabel { + icon: FaHourglassEnd, + r#for: "input_category_waiting_for", }, - input { + Input { + class: "grow", name: "category_waiting_for", required: true, initial_value: waiting_for, r#type: "text", - class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", - id: "input_category_waiting_for" - }, + } } }, Category::Calendar { date, reoccurrence, time } => rsx! { div { class: "flex flex-row items-center gap-3", - label { - r#for: "input_category_calendar_date", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaClock, - height: 16, - width: 16 - } + InputLabel { + icon: FaClock, + r#for: "input_category_calendar_date" }, div { - class: "grow flex flex-row gap-2", - input { - r#type: "date", + class: "grow grid grid-cols-2 gap-3", + Input { + class: "grow", name: "category_calendar_date", + r#type: "date", required: true, initial_value: date.format("%Y-%m-%d").to_string(), - class: - "py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer", - id: "input_category_calendar_date" }, - input { - r#type: "time", + Input { + class: "grow", name: "category_calendar_time", + r#type: "time", initial_value: time.map(|calendar_time| calendar_time.time.format("%H:%M").to_string() ), - class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow cursor-pointer", - id: "input_category_calendar_time", - oninput: move |event| { + oninput: move |event: Event| { category_calendar_has_time.set(!event.value().is_empty()); } } @@ -311,60 +269,46 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() }, div { class: "flex flex-row items-center gap-3", - label { - r#for: "category_calendar_reoccurrence_length", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaRepeat, - height: 16, - width: 16 - } + InputLabel { + icon: FaRepeat, + r#for: "category_calendar_reoccurrence_length" }, div { - class: "grow grid grid-cols-6 gap-2", + class: "grow grid grid-cols-5 items-end gap-3", ReoccurrenceIntervalInput { reoccurrence_interval: category_calendar_reoccurrence_interval }, - input { + Input { + class: "text-right", r#type: "number", inputmode: "numeric", name: "category_calendar_reoccurrence_length", disabled: category_calendar_reoccurrence_interval().is_none(), required: true, - min: 1, + min: "1", initial_value: category_calendar_reoccurrence_interval().map_or( String::new(), |_| reoccurrence.map_or(1, |reoccurrence| reoccurrence.length).to_string() - ), - class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right", - id: "category_calendar_reoccurrence_length" + ) } } }, if category_calendar_has_time() { div { class: "flex flex-row items-center gap-3", - label { - r#for: "category_calendar_reminder_offset_index", - class: "min-w-6 flex flex-row justify-center items-center", - Icon { - class: "text-zinc-400/50", - icon: FaBell, - height: 16, - width: 16 - } + InputLabel { + 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 input-range-reverse cursor-pointer", - id: "category_calendar_has_reminder", oninput: move |event| { category_calendar_reminder_offset_index.set( event.value().parse().unwrap() @@ -372,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 { @@ -399,17 +343,20 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() } } } - 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 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(), @@ -417,27 +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: FaTrashCan, - height: 16, - width: 16 } } - button { - form: "form_task", - r#type: "submit", - class: "py-3 px-4 flex flex-row justify-center items-center bg-zinc-300/50 rounded-lg 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 98a4931..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::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,28 +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: "text-zinc-500", + 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. @@ -50,23 +53,21 @@ 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 { - Icon { - class: "cursor-pointer", - icon: FaSquare, - height: 30, - width: 30 - } + 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)]" + } + ) } }, - TaskListItem { - task: task.clone() + div { + 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 f0800ad..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: "animate-[spin_2000ms_linear_infinite]", - icon: FaCog, - height: 32, - width: 32 - } - } - } - }, - Outlet:: {} - } - StickyBottom { - FormOpenButton { - 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_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 {} + } + } +} diff --git a/tailwind.css b/tailwind.css index 664bee6..225f3b1 100644 --- a/tailwind.css +++ b/tailwind.css @@ -20,4 +20,8 @@ /* stylelint-disable-next-line */ @theme { --font-sans: "Inter", "sans"; + --color-amber-300-muted: #b89a2e; + --color-amber-700-muted: #80390b; + --color-gray-800-muted: #141d2d; + --color-gray-900-muted: #0b111f; }