From 28143a70882a6d45870212c3b12d3c4ddc8b18e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Sat, 31 Aug 2024 10:47:49 +0200 Subject: [PATCH] feat: ability to view tasks in different categories --- src/components/app.rs | 2 +- src/components/bottom_panel.rs | 44 ++ src/components/category_input.rs | 104 +++++ src/components/create_task_button.rs | 19 + src/components/home.rs | 4 - src/components/layout.rs | 30 ++ src/components/mod.rs | 10 + src/components/navigation.rs | 83 ++++ src/components/navigation_item.rs | 21 + .../pages/category_calendar_page.rs | 31 ++ src/components/pages/category_done_page.rs | 27 ++ src/components/pages/category_inbox_page.rs | 27 ++ .../pages/category_long_term_page.rs | 27 ++ .../pages/category_next_steps_page.rs | 27 ++ .../pages/category_someday_maybe_page.rs | 27 ++ src/components/pages/category_today_page.rs | 39 ++ src/components/pages/category_trash_page.rs | 27 ++ .../pages/category_waiting_for_page.rs | 27 ++ src/components/pages/mod.rs | 11 + src/components/pages/not_found_page.rs | 11 + src/components/pages/projects_page.rs | 7 + src/components/reoccurrence_input.rs | 79 ++++ src/components/sticky_bottom.rs | 14 + src/components/task_form.rs | 376 +++++++++++------- src/components/task_list.rs | 66 +++ src/models/category.rs | 86 +++- src/models/task.rs | 2 +- src/route/mod.rs | 44 +- src/server/tasks.rs | 24 +- 29 files changed, 1127 insertions(+), 169 deletions(-) create mode 100644 src/components/bottom_panel.rs create mode 100644 src/components/category_input.rs create mode 100644 src/components/create_task_button.rs create mode 100644 src/components/layout.rs create mode 100644 src/components/navigation.rs create mode 100644 src/components/navigation_item.rs create mode 100644 src/components/pages/category_calendar_page.rs create mode 100644 src/components/pages/category_done_page.rs create mode 100644 src/components/pages/category_inbox_page.rs create mode 100644 src/components/pages/category_long_term_page.rs create mode 100644 src/components/pages/category_next_steps_page.rs create mode 100644 src/components/pages/category_someday_maybe_page.rs create mode 100644 src/components/pages/category_today_page.rs create mode 100644 src/components/pages/category_trash_page.rs create mode 100644 src/components/pages/category_waiting_for_page.rs create mode 100644 src/components/pages/mod.rs create mode 100644 src/components/pages/not_found_page.rs create mode 100644 src/components/pages/projects_page.rs create mode 100644 src/components/reoccurrence_input.rs create mode 100644 src/components/sticky_bottom.rs create mode 100644 src/components/task_list.rs diff --git a/src/components/app.rs b/src/components/app.rs index 0ccd3a2..5597b7e 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -7,7 +7,7 @@ use dioxus::prelude::*; pub(crate) fn App() -> Element { rsx! { div { - class: "min-h-screen text-white bg-neutral-800", + class: "min-h-screen text-zinc-200 bg-zinc-800", Router:: {} } } diff --git a/src/components/bottom_panel.rs b/src/components/bottom_panel.rs new file mode 100644 index 0000000..1332e78 --- /dev/null +++ b/src/components/bottom_panel.rs @@ -0,0 +1,44 @@ +use std::thread::sleep; +use dioxus::prelude::*; +use crate::components::navigation::Navigation; +use crate::components::task_form::TaskForm; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; + +#[component] +pub(crate) fn BottomPanel(creating_task: bool) -> Element { + let mut expanded = use_signal(|| creating_task); + let navigation_expanded = use_signal(|| false); + + use_effect(use_reactive(&creating_task, move |creating_task| { + if creating_task { + expanded.set(true); + } else { + spawn(async move { + async_std::task::sleep(std::time::Duration::from_millis(500)).await; + expanded.set(false); + }); + } + })); + + rsx! { + div { + class: format!( + "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)] {}", + match (creating_task, navigation_expanded()) { + (false, false) => "h-[64px]", + (false, true) => "h-[128px]", + (true, _) => "h-[448px]", + } + ), + if expanded() { + TaskForm {} + } else { + Navigation { + expanded: navigation_expanded, + } + } + } + } +} diff --git a/src/components/category_input.rs b/src/components/category_input.rs new file mode 100644 index 0000000..1ea3b8d --- /dev/null +++ b/src/components/category_input.rs @@ -0,0 +1,104 @@ +use crate::models::category::Category; +use crate::server::tasks::get_tasks_in_category; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use std::fmt::format; + +#[component] +pub(crate) fn CategoryInput(selected_category: Signal, class: Option<&'static str>) -> Element { + rsx! { + div { + class: format!("flex flex-row gap-2 {}", class.unwrap_or("")), + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if selected_category() == Category::SomedayMaybe { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::SomedayMaybe); + }, + i { + class: "fa-solid fa-question" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if selected_category() == Category::LongTerm { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::LongTerm); + }, + i { + class: "fa-solid fa-water" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if let Category::WaitingFor(_) = selected_category() { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::WaitingFor(String::new())); + }, + i { + class: "fa-solid fa-hourglass-half" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if selected_category() == Category::NextSteps { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::NextSteps); + }, + i { + class: "fa-solid fa-forward" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if let Category::Calendar { .. } = selected_category() { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::Calendar { + date: NaiveDate::default(), + reoccurrence: None, + time: None, + }); + }, + i { + class: "fa-solid fa-calendar-days" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg grow basis-0 {}", + if selected_category() == Category::Inbox { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + selected_category.set(Category::Inbox); + }, + i { + class: "fa-solid fa-inbox" + } + } + } + } +} diff --git a/src/components/create_task_button.rs b/src/components/create_task_button.rs new file mode 100644 index 0000000..b87a855 --- /dev/null +++ b/src/components/create_task_button.rs @@ -0,0 +1,19 @@ +use dioxus::prelude::*; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; + +#[component] +pub(crate) fn CreateTaskButton(creating: Signal) -> Element { + rsx! { + button { + class: "m-4 py-3 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", + onclick: move |_| { + creating.set(!creating()); + }, + i { + class: format!("min-w-6 fa-solid {}", if creating() { "fa-xmark" } else { "fa-plus" }), + } + } + } +} diff --git a/src/components/home.rs b/src/components/home.rs index 827fad4..4dd9aac 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -1,13 +1,9 @@ -use crate::components::project_form::ProjectForm; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; -use crate::components::task_form::TaskForm; #[component] pub(crate) fn Home() -> Element { rsx! { - ProjectForm {} - TaskForm {} } } diff --git a/src/components/layout.rs b/src/components/layout.rs new file mode 100644 index 0000000..5ddb57c --- /dev/null +++ b/src/components/layout.rs @@ -0,0 +1,30 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn Layout() -> Element { + let creating_task = use_signal(|| false); + + rsx! { + Outlet:: {} + StickyBottom { + CreateTaskButton { + creating: creating_task, + } + BottomPanel { + creating_task: creating_task(), + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index c632a34..e60f716 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -2,3 +2,13 @@ pub(crate) mod app; pub(crate) mod home; pub(crate) mod project_form; pub(crate) mod task_form; +pub(crate) mod task_list; +pub(crate) mod pages; +pub(crate) mod navigation; +pub(crate) mod create_task_button; +pub(crate) mod bottom_panel; +pub(crate) mod sticky_bottom; +pub(crate) mod category_input; +pub(crate) mod reoccurrence_input; +pub(crate) mod layout; +mod navigation_item; diff --git a/src/components/navigation.rs b/src/components/navigation.rs new file mode 100644 index 0000000..8104116 --- /dev/null +++ b/src/components/navigation.rs @@ -0,0 +1,83 @@ +use crate::components::navigation_item::NavigationItem; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use dioxus::prelude::*; + +#[component] +pub(crate) fn Navigation(expanded: Signal) -> Element { + rsx! { + div { + class: "grid grid-cols-5 justify-stretch", + button { + class: format!( + "py-4 text-center text-2xl {}", + if expanded() { "text-zinc-200" } + else { "text-zinc-500" } + ), + onclick: move |_| expanded.set(!expanded()), + i { + class: "fa-solid fa-bars" + } + }, + NavigationItem { + route: Route::CategoryNextStepsPage, + i { + class: "fa-solid fa-forward" + } + }, + NavigationItem { + route: Route::CategoryCalendarPage, + i { + class: "fa-solid fa-calendar-days" + } + }, + NavigationItem { + route: Route::CategoryTodayPage, + i { + class: "fa-solid fa-calendar-day" + } + }, + NavigationItem { + route: Route::CategoryInboxPage, + i { + class: "fa-solid fa-inbox" + } + }, + {if expanded() { + rsx! { + NavigationItem { + route: Route::ProjectsPage, + i { + class: "fa-solid fa-list" + } + }, + NavigationItem { + route: Route::CategoryTrashPage, + i { + class: "fa-solid fa-trash-can" + } + }, + NavigationItem { + route: Route::CategoryDonePage, + i { + class: "fa-solid fa-check" + } + }, + NavigationItem { + route: Route::CategoryLongTermPage, + i { + class: "fa-solid fa-water" + } + }, + NavigationItem { + route: Route::CategoryWaitingForPage, + i { + class: "fa-solid fa-hourglass-half" + } + } + } + } else { None }} + } + } +} diff --git a/src/components/navigation_item.rs b/src/components/navigation_item.rs new file mode 100644 index 0000000..3393245 --- /dev/null +++ b/src/components/navigation_item.rs @@ -0,0 +1,21 @@ +use dioxus::prelude::*; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; + +#[component] +pub(crate) fn NavigationItem(route: Route, children: Element) -> Element { + let current_route = use_route::(); + + rsx! { + Link { + to: route.clone(), + class: format!( + "py-4 text-center text-2xl {}", + if current_route == route { "text-zinc-200" } + else { "text-zinc-500" } + ), + children + } + } +} diff --git a/src/components/pages/category_calendar_page.rs b/src/components/pages/category_calendar_page.rs new file mode 100644 index 0000000..6c434ce --- /dev/null +++ b/src/components/pages/category_calendar_page.rs @@ -0,0 +1,31 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryCalendarPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::Calendar { + date: NaiveDate::default(), + reoccurrence: None, + time: None, + }) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_done_page.rs b/src/components/pages/category_done_page.rs new file mode 100644 index 0000000..b524ec8 --- /dev/null +++ b/src/components/pages/category_done_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryDonePage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::Done) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_inbox_page.rs b/src/components/pages/category_inbox_page.rs new file mode 100644 index 0000000..ca592bc --- /dev/null +++ b/src/components/pages/category_inbox_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryInboxPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::Inbox) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_long_term_page.rs b/src/components/pages/category_long_term_page.rs new file mode 100644 index 0000000..4cc90de --- /dev/null +++ b/src/components/pages/category_long_term_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryLongTermPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::LongTerm) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_next_steps_page.rs b/src/components/pages/category_next_steps_page.rs new file mode 100644 index 0000000..bfbe17f --- /dev/null +++ b/src/components/pages/category_next_steps_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryNextStepsPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::NextSteps) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_someday_maybe_page.rs b/src/components/pages/category_someday_maybe_page.rs new file mode 100644 index 0000000..bb70a17 --- /dev/null +++ b/src/components/pages/category_someday_maybe_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategorySomedayMaybePage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::SomedayMaybe) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_today_page.rs b/src/components/pages/category_today_page.rs new file mode 100644 index 0000000..d01f477 --- /dev/null +++ b/src/components/pages/category_today_page.rs @@ -0,0 +1,39 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::navigation::Navigation; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::models::task::Task; +use crate::route::Route; +use crate::schema::tasks::category; +use crate::server::tasks::get_tasks_in_category; +use chrono::{Local, NaiveDate}; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; + +#[component] +pub(crate) fn CategoryTodayPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::Calendar { + date: NaiveDate::default(), + reoccurrence: None, + time: None, + }) + )?.unwrap().unwrap().iter().filter(|task| { + if let Category::Calendar { date, .. } = task.category() { + *date == Local::now().date_naive() + } else { + panic!("Unexpected category."); + } + }).cloned().collect::>(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_trash_page.rs b/src/components/pages/category_trash_page.rs new file mode 100644 index 0000000..538c85d --- /dev/null +++ b/src/components/pages/category_trash_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryTrashPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::Trash) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/category_waiting_for_page.rs b/src/components/pages/category_waiting_for_page.rs new file mode 100644 index 0000000..3145bfc --- /dev/null +++ b/src/components/pages/category_waiting_for_page.rs @@ -0,0 +1,27 @@ +use crate::components::bottom_panel::BottomPanel; +use crate::components::navigation::Navigation; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use crate::components::create_task_button::CreateTaskButton; +use crate::components::sticky_bottom::StickyBottom; +use crate::components::task_form::TaskForm; +use crate::server::tasks::get_tasks_in_category; + +#[component] +pub(crate) fn CategoryWaitingForPage() -> Element { + let tasks = use_server_future( + move || get_tasks_in_category(Category::WaitingFor(String::new())) + )?.unwrap().unwrap(); + + rsx! { + TaskList { + tasks: tasks, + class: "pb-36" + } + } +} diff --git a/src/components/pages/mod.rs b/src/components/pages/mod.rs new file mode 100644 index 0000000..59bb5ef --- /dev/null +++ b/src/components/pages/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod category_inbox_page; +pub(crate) mod category_calendar_page; +pub(crate) mod category_today_page; +pub(crate) mod category_waiting_for_page; +pub(crate) mod category_long_term_page; +pub(crate) mod category_next_steps_page; +pub(crate) mod category_someday_maybe_page; +pub(crate) mod category_done_page; +pub(crate) mod category_trash_page; +pub(crate) mod not_found_page; +pub(crate) mod projects_page; diff --git a/src/components/pages/not_found_page.rs b/src/components/pages/not_found_page.rs new file mode 100644 index 0000000..73e3ebd --- /dev/null +++ b/src/components/pages/not_found_page.rs @@ -0,0 +1,11 @@ +use dioxus::prelude::*; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; + +#[component] +pub(crate) fn NotFoundPage(route: Vec) -> Element { + rsx! { + {"404"} + } +} diff --git a/src/components/pages/projects_page.rs b/src/components/pages/projects_page.rs new file mode 100644 index 0000000..5c8b018 --- /dev/null +++ b/src/components/pages/projects_page.rs @@ -0,0 +1,7 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn ProjectsPage() -> Element { + rsx! { + } +} diff --git a/src/components/reoccurrence_input.rs b/src/components/reoccurrence_input.rs new file mode 100644 index 0000000..30099a3 --- /dev/null +++ b/src/components/reoccurrence_input.rs @@ -0,0 +1,79 @@ +use crate::models::category::{Category, Reoccurrence, ReoccurrenceInterval}; +use crate::server::tasks::get_tasks_in_category; +use chrono::NaiveDate; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use std::fmt::format; + +#[component] +pub(crate) fn ReoccurrenceIntervalInput( + reoccurrence_interval: Signal>, + class_buttons: Option<&'static str> +) -> Element { + rsx! { + button { + r#type: "button", + class: format!( + "py-2 rounded-lg {} {}", + class_buttons.unwrap_or(""), + if reoccurrence_interval().is_none() { "bg-zinc-500/50" } + else { "bg-zinc-800/50" } + ), + onclick: move |_| { + reoccurrence_interval.set(None); + }, + i { + class: "fa-solid fa-ban" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg {} {}", + 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)) + }, + i { + class: "fa-solid fa-sun" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg {} {}", + 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)) + }, + i { + class: "fa-solid fa-moon" + } + }, + button { + r#type: "button", + class: format!( + "py-2 rounded-lg {} {}", + 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)) + }, + i { + class: "fa-solid fa-earth-europe" + } + } + } +} diff --git a/src/components/sticky_bottom.rs b/src/components/sticky_bottom.rs new file mode 100644 index 0000000..9dbb9d7 --- /dev/null +++ b/src/components/sticky_bottom.rs @@ -0,0 +1,14 @@ +use dioxus::prelude::*; +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::route::Route; + +#[component] +pub(crate) fn StickyBottom(children: Element) -> Element { + rsx! { + div { + class: "fixed bottom-0 left-0 right-0 flex flex-col", + {children} + } + } +} diff --git a/src/components/task_form.rs b/src/components/task_form.rs index a280437..981dcd8 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -1,45 +1,65 @@ -use chrono::Duration; -use crate::models::category::{CalendarTime, Category}; +use std::fmt::Display; +use crate::components::category_input::CategoryInput; +use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; +use crate::models::category::{CalendarTime, Category, Reoccurrence, ReoccurrenceInterval}; use crate::models::task::NewTask; use crate::server::projects::get_projects; use crate::server::tasks::create_task; +use chrono::{Duration, NaiveDate}; use dioxus::core_macro::{component, rsx}; use dioxus::dioxus_core::Element; use dioxus::prelude::*; +use crate::route::Route; + +const REMINDER_OFFSETS: [Option; 17] = [ + None, + Some(Duration::days(1)), + Some(Duration::hours(12)), + Some(Duration::hours(11)), + Some(Duration::hours(10)), + Some(Duration::hours(9)), + Some(Duration::hours(8)), + Some(Duration::hours(7)), + Some(Duration::hours(6)), + Some(Duration::hours(5)), + Some(Duration::hours(4)), + Some(Duration::hours(3)), + Some(Duration::hours(2)), + Some(Duration::hours(1)), + Some(Duration::minutes(30)), + Some(Duration::minutes(10)), + Some(Duration::zero()), +]; #[component] pub(crate) fn TaskForm() -> Element { - let categories = vec![ - Category::Inbox, - Category::SomedayMaybe, - Category::WaitingFor(String::new()), - Category::NextSteps, - Category::Calendar { - date: chrono::Local::now().date_naive(), - reoccurance_interval: None, - time: None, - }, - Category::LongTerm, - ]; let projects = use_server_future(get_projects)?.unwrap().unwrap(); - let mut selected_category_index = use_signal::(|| 0); - let mut category_calendar_is_reoccurring = use_signal::(|| false); - let mut category_calendar_has_time = use_signal::(|| false); - let mut category_calendar_has_reminder = use_signal::(|| false); + let route = use_route::(); + let mut selected_category = use_signal(|| match route { + Route::CategorySomedayMaybePage => Category::SomedayMaybe, + Route::CategoryWaitingForPage => Category::WaitingFor(String::new()), + Route::CategoryNextStepsPage => Category::NextSteps, + Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar { + date: NaiveDate::default(), + reoccurrence: None, + time: None, + }, + Route::CategoryLongTermPage => Category::LongTerm, + _ => Category::Inbox, + }); + let mut category_calendar_reoccurrence_interval = use_signal(|| None); + let mut category_calendar_has_time = use_signal(|| false); + let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1); rsx! { form { onsubmit: move |event| { - let categories = categories.clone(); async move { let new_task = NewTask::new( event.values().get("title").unwrap().as_value(), event.values().get("deadline").unwrap().as_value().parse().ok(), - match &categories[ - event.values().get("category_index").unwrap() - .as_value().parse::().unwrap() - ] { + match &selected_category() { Category::WaitingFor(_) => Category::WaitingFor( event.values().get("category_waiting_for").unwrap() .as_value() @@ -47,24 +67,24 @@ pub(crate) fn TaskForm() -> Element { Category::Calendar { .. } => Category::Calendar { date: event.values().get("category_calendar_date").unwrap() .as_value().parse().unwrap(), - reoccurance_interval: - event.values().get("category_calendar_is_reoccurring").map( - |_| Duration::days( - event.values().get("category_calendar_reoccurance_interval") + reoccurrence: category_calendar_reoccurrence_interval().map( + |reoccurrence_interval| Reoccurrence::new( + event.values().get("category_calendar_date").unwrap() + .as_value().parse().unwrap(), + reoccurrence_interval, + event.values().get("category_calendar_reoccurrence_length") .unwrap().as_value().parse().unwrap() ) ), time: event.values().get("category_calendar_time").unwrap() - .as_value().parse().ok().map(|time| + .as_value().parse().ok().map(|time| CalendarTime::new( time, - event.values().get("category_calendar_has_reminder").map( - |_| Duration::minutes( - event.values() - .get("category_calendar_reminder_offset").unwrap() - .as_value().parse().unwrap() - ) - ) + REMINDER_OFFSETS[ + event.values() + .get("category_calendar_reminder_offset_index").unwrap() + .as_value().parse::().unwrap() + ] ) ) }, @@ -77,145 +97,207 @@ pub(crate) fn TaskForm() -> Element { } }, class: "p-4 flex flex-col gap-4", - input { - r#type: "text", - name: "title", - required: true, - placeholder: "title", - class: "p-2 bg-neutral-700 rounded", - }, - select { - name: "category_index", - oninput: move |event| { - selected_category_index.set(event.value().parse().unwrap()); - }, - class: "p-2 bg-neutral-700 rounded", - option { - value: 0, - "inbox" - }, - option { - value: 1, - "someday maybe" - }, - option { - value: 2, - "waiting for" - }, - option { - value: 3, - "next steps" - }, - option { - value: 4, - "calendar" - }, - option { - value: 5, - "long term" - }, - }, - match categories[selected_category_index()] { - Category::WaitingFor(_) => rsx !{ - input { - r#type: "text", - name: "category_waiting_for", - required: true, - class: "p-2 bg-neutral-700 rounded", + div { + class: "flex flex-row items-center gap-3", + label { + r#for: "input_title", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-pen-clip text-zinc-400/50" }, }, - Category::Calendar { .. } => rsx !{ - input { - r#type: "date", - name: "category_calendar_date", - required: true, - class: "p-2 bg-neutral-700 rounded", + input { + r#type: "text", + name: "title", + required: true, + class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", + id: "input_title" + }, + }, + div { + class: "flex flex-row items-center gap-3", + label { + r#for: "input_project", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-list text-zinc-400/50" + } + }, + select { + name: "project_id", + class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow", + id: "input_project", + option { + value: 0, + "None" }, + for project in projects { + option { + value: project.id().to_string(), + {project.title()} + } + } + }, + }, + div { + class: "flex flex-row items-center gap-3", + label { + r#for: "input_deadline", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-bomb text-zinc-400/50" + } + }, + input { + r#type: "date", + name: "deadline", + class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0", + id: "input_deadline" + } + }, + div { + class: "flex flex-row items-center gap-3", + label { + class: "min-w-6 text-center", + i { + class: "fa-solid fa-layer-group text-zinc-400/50" + } + }, + CategoryInput { + selected_category: selected_category.clone(), + class: "grow" + } + } + match selected_category() { + Category::WaitingFor(_) => rsx! { div { - input { - r#type: "checkbox", - name: "category_calendar_is_reoccurring", - id: "category_calendar_is_reoccurring", - onchange: move |event| { - category_calendar_is_reoccurring.set(event.checked()); + class: "flex flex-row items-center gap-3", + label { + r#for: "input_deadline", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-hourglass-end text-zinc-400/50" } }, - label { - r#for: "category_calendar_is_reoccurring", - " is reoccurring" - } - }, - if category_calendar_is_reoccurring() { input { - r#type: "number", - name: "category_calendar_reoccurance_interval", + r#type: "text", + name: "category_waiting_for", required: true, - min: 1, - placeholder: "reoccurance interval (days)", - class: "p-2 bg-neutral-700 rounded", + class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", + id: "input_category_waiting_for" + }, + } + }, + Category::Calendar { .. } => rsx! { + div { + class: "flex flex-row items-center gap-3", + label { + r#for: "input_category_calendar_date", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-clock text-zinc-400/50" + } + }, + div { + class: "grow flex flex-row gap-2", + input { + r#type: "date", + name: "category_calendar_date", + required: true, + initial_value: chrono::Local::now().format("%Y-%m-%d").to_string(), + class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", + id: "input_category_calendar_date" + }, + input { + r#type: "time", + name: "category_calendar_time", + class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", + id: "input_category_calendar_time", + oninput: move |event| { + category_calendar_has_time.set(!event.value().is_empty()); + } + } } }, - input { - r#type: "time", - name: "category_calendar_time", - class: "p-2 bg-neutral-700 rounded", - oninput: move |event| { - category_calendar_has_time.set(!event.value().is_empty()); + div { + class: "flex flex-row items-center gap-3", + label { + r#for: "category_calendar_reoccurrence_length", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-repeat text-zinc-400/50" + } + }, + div { + class: "grow grid grid-cols-6 gap-2", + ReoccurrenceIntervalInput { + reoccurrence_interval: category_calendar_reoccurrence_interval + }, + input { + r#type: "number", + inputmode: "numeric", + name: "category_calendar_reoccurrence_length", + disabled: category_calendar_reoccurrence_interval().is_none(), + required: true, + min: 1, + initial_value: + if category_calendar_reoccurrence_interval().is_none() { "" } + else { "1" }, + 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 text-center", + i { + class: "fa-solid fa-bell text-zinc-400/50" + } + }, input { - r#type: "checkbox", - name: "category_calendar_has_reminder", - value: 0, + r#type: "range", + name: "category_calendar_reminder_offset_index", + min: 0, + max: REMINDER_OFFSETS.len() as i64 - 1, + initial_value: REMINDER_OFFSETS.len() as i64 - 1, + class: "grow input-range-reverse", id: "category_calendar_has_reminder", - onchange: move |event| { - category_calendar_has_reminder.set(event.checked()); + oninput: move |event| { + category_calendar_reminder_offset_index.set( + event.value().parse().unwrap() + ); } }, label { - r#for: "category_calendar_has_reminder", - " set a reminder" + r#for: "category_calendar_reminder_offset_index", + class: "pr-3 min-w-16 text-right", + {REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map( + |offset| if offset.num_hours() < 1 { + format!("{} min", offset.num_minutes()) + } else { + format!("{} h", offset.num_hours()) + } + ).unwrap_or_else(|| "none".to_string())} } } } - if category_calendar_has_reminder() { - input { - r#type: "number", - name: "category_calendar_reminder_offset", - required: true, - min: 0, - placeholder: "reminder offset (minutes)", - class: "p-2 bg-neutral-700 rounded", - } - } }, _ => None }, - input { - r#type: "date", - name: "deadline", - class: "p-2 bg-neutral-700 rounded", - }, - select { - name: "project_id", - class: "p-2 bg-neutral-700 rounded", - option { - value: 0, - "none" - }, - for project in projects { - option { - value: project.id().to_string(), - {project.title()} + div { + class: "flex flex-row justify-end mt-auto", + button { + r#type: "submit", + class: "py-2 px-4 bg-zinc-300/50 rounded-lg", + i { + class: "fa-solid fa-floppy-disk" } } - }, - button { - r#type: "submit", - "create" } } -} + } } diff --git a/src/components/task_list.rs b/src/components/task_list.rs new file mode 100644 index 0000000..0612a2e --- /dev/null +++ b/src/components/task_list.rs @@ -0,0 +1,66 @@ +use crate::models::category::Category; +use crate::models::task::Task; +use crate::server::tasks::get_tasks_in_category; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; + +#[component] +pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element { + rsx! { + div { + class: format!("pt-3 px-8 flex flex-col {}", class.unwrap_or("")), + for task in tasks { + div { + class: format!( + "pt-5 {} flex flex-row gap-4", + if task.deadline().is_some() { + "pb-0.5" + } else if let Category::Calendar { time, .. } = task.category() { + if time.is_some() { + "pb-0.5" + } else { + "pb-5" + } + } else { + "pb-5" + } + ), + i { + class: "fa-regular fa-square text-3xl text-zinc-600", + }, + div { + class: "flex flex-col", + div { + class: "mt-1 grow", + {task.title()} + }, + div { + class: "flex flex-row gap-3", + if let Some(deadline) = task.deadline() { + div { + class: "text-sm text-zinc-400", + i { + class: "fa-solid fa-bomb" + }, + {deadline.format(" %m. %d.").to_string()} + } + } + if let Category::Calendar { time, .. } = task.category() { + if let Some(calendar_time) = time { + div { + class: "text-sm text-zinc-400", + i { + class: "fa-solid fa-clock" + }, + {calendar_time.time().format(" %k:%M").to_string()} + } + } + } + } + } + } + } + } + } +} diff --git a/src/models/category.rs b/src/models/category.rs index c6ca42c..ad35ba6 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -1,10 +1,12 @@ +use crate::schema::tasks; use chrono::{Duration, NaiveDate, NaiveTime}; use diesel::deserialize::FromSql; use diesel::pg::{Pg, PgValue}; use diesel::serialize::{Output, ToSql}; -use diesel::sql_types::Jsonb; -use diesel::{AsExpression, FromSqlRow}; +use diesel::sql_types::{Bool, Jsonb}; +use diesel::{AsExpression, BoxableExpression, FromSqlRow, PgJsonbExpressionMethods}; use serde::{Deserialize, Serialize}; +use serde_json::json; use serde_with::DurationSeconds; use std::io::Write; @@ -18,8 +20,7 @@ pub enum Category { NextSteps, Calendar { date: NaiveDate, - #[serde_as(as = "Option>")] - reoccurance_interval: Option, + reoccurrence: Option, time: Option, }, LongTerm, @@ -27,17 +28,26 @@ pub enum Category { Trash, } -#[serde_with::serde_as] -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct CalendarTime { - time: NaiveTime, - #[serde_as(as = "Option>")] - reminder_offset: Option, +impl Category { + pub fn eq_sql_predicate(&self) -> Box> { + use crate::schema::tasks::dsl::*; + + match self { + Category::Inbox => Box::new(category.contains(json!("Inbox"))), + Category::SomedayMaybe => Box::new(category.contains(json!("SomedayMaybe"))), + Category::WaitingFor(_) => Box::new(category.has_key("WaitingFor")), + Category::NextSteps => Box::new(category.contains(json!("NextSteps"))), + Category::Calendar { .. } => Box::new(category.has_key("Calendar")), + Category::LongTerm => Box::new(category.contains(json!("LongTerm"))), + Category::Done => Box::new(category.contains(json!("Done"))), + Category::Trash => Box::new(category.contains(json!("Trash"))), + } + } } -impl CalendarTime { - pub fn new(time: NaiveTime, reminder_offset: Option) -> Self { - Self { time, reminder_offset } +impl PartialEq for Category { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) } } @@ -63,3 +73,53 @@ impl FromSql for Category { serde_json::from_str(str).map_err(Into::into) } } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum ReoccurrenceInterval { + Day, + Month, + Year, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Reoccurrence { + start_date: NaiveDate, + interval: ReoccurrenceInterval, + length: u32, +} + +impl Reoccurrence { + pub fn new(start_date: NaiveDate, interval: ReoccurrenceInterval, length: u32) -> Self { + Self { start_date, interval, length } + } + + pub fn interval(&self) -> &ReoccurrenceInterval { + &self.interval + } + + pub fn length(&self) -> u32 { + self.length + } +} + +#[serde_with::serde_as] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CalendarTime { + time: NaiveTime, + #[serde_as(as = "Option>")] + reminder_offset: Option, +} + +impl CalendarTime { + pub fn new(time: NaiveTime, reminder_offset: Option) -> Self { + Self { time, reminder_offset } + } + + pub fn time(&self) -> NaiveTime { + self.time + } + + pub fn reminder_offset(&self) -> Option { + self.reminder_offset + } +} diff --git a/src/models/task.rs b/src/models/task.rs index d67256a..058874d 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -7,7 +7,7 @@ use crate::schema::tasks; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)] +#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)] #[diesel(table_name = crate::schema::tasks)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Task { diff --git a/src/route/mod.rs b/src/route/mod.rs index 275f297..8beedcf 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -1,8 +1,48 @@ use crate::components::home::Home; +use crate::components::pages::category_inbox_page::CategoryInboxPage; +use crate::components::pages::category_next_steps_page::CategoryNextStepsPage; +use crate::components::pages::category_today_page::CategoryTodayPage; +use crate::components::pages::category_trash_page::CategoryTrashPage; +use crate::components::pages::category_waiting_for_page::CategoryWaitingForPage; +use crate::components::pages::category_someday_maybe_page::CategorySomedayMaybePage; +use crate::components::pages::category_done_page::CategoryDonePage; +use crate::components::pages::category_calendar_page::CategoryCalendarPage; +use crate::components::pages::category_long_term_page::CategoryLongTermPage; +use crate::components::pages::projects_page::ProjectsPage; +use crate::components::pages::not_found_page::NotFoundPage; +use crate::components::layout::Layout; use dioxus::prelude::*; +use crate::models::category::Category; #[derive(Clone, Routable, Debug, PartialEq)] +#[rustfmt::skip] pub(crate) enum Route { - #[route("/")] - Home {}, + #[layout(Layout)] + #[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("/long-term")] + CategoryLongTermPage, + #[route("/done")] + CategoryDonePage, + #[route("/trash")] + CategoryTrashPage, + #[route("/projects")] + ProjectsPage, + #[end_layout] + #[redirect("/", || Route::CategoryTodayPage)] + #[route("/:..route")] + NotFoundPage { + route: Vec, + }, } diff --git a/src/server/tasks.rs b/src/server/tasks.rs index 0b7c086..62c383a 100644 --- a/src/server/tasks.rs +++ b/src/server/tasks.rs @@ -2,10 +2,11 @@ use crate::errors::error::Error; use crate::errors::error_vec::ErrorVec; use crate::models::task::{NewTask, Task}; use crate::server::database_connection::establish_database_connection; -use diesel::{RunQueryDsl, SelectableHelper}; +use diesel::{QueryDsl, RunQueryDsl, SelectableHelper}; use dioxus::prelude::*; use validator::Validate; use crate::errors::task_create_error::TaskCreateError; +use crate::models::category::Category; #[server] pub(crate) async fn create_task(new_task: NewTask) @@ -43,3 +44,24 @@ pub(crate) async fn create_task(new_task: NewTask) Ok(new_task) } + +#[server] +pub(crate) async fn get_tasks_in_category(filtered_category: Category) + -> Result, ServerFnError>> { + use crate::schema::tasks::dsl::*; + + let mut connection = establish_database_connection() + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + let results = tasks + .select(Task::as_select()) + .filter(filtered_category.eq_sql_predicate()) + .load::(&mut connection) + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + Ok(results) +}