From 270e06de462d0b723caeb9ba2fbba2639049c49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Thu, 5 Sep 2024 17:19:35 +0200 Subject: [PATCH] feat: use Dioxus query for fetching data --- src/components/app.rs | 6 +- src/components/mod.rs | 2 +- .../pages/category_calendar_page.rs | 84 +++++++-- src/components/pages/category_done_page.rs | 19 +- src/components/pages/category_inbox_page.rs | 19 +- .../pages/category_long_term_page.rs | 19 +- .../pages/category_next_steps_page.rs | 19 +- src/components/pages/category_page.rs | 32 ++++ .../pages/category_someday_maybe_page.rs | 19 +- src/components/pages/category_today_page.rs | 173 +++++++++++++++--- src/components/pages/category_trash_page.rs | 19 +- .../pages/category_waiting_for_page.rs | 19 +- src/components/pages/mod.rs | 1 + src/components/task_form.rs | 9 +- src/components/task_list.rs | 8 +- src/models/category.rs | 10 +- src/models/task.rs | 1 + 17 files changed, 290 insertions(+), 169 deletions(-) create mode 100644 src/components/pages/category_page.rs diff --git a/src/components/app.rs b/src/components/app.rs index 5597b7e..708b0f5 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -2,12 +2,16 @@ use crate::route::Route; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; +use dioxus_query::prelude::{use_init_query_client}; +use crate::query::{QueryErrors, QueryKey, QueryValue}; #[component] pub(crate) fn App() -> Element { + use_init_query_client::(); + rsx! { div { - class: "min-h-screen text-zinc-200 bg-zinc-800", + class: "min-h-screen text-zinc-200 bg-zinc-800 pt-4 pb-36", Router:: {} } } diff --git a/src/components/mod.rs b/src/components/mod.rs index e60f716..bd961d8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -11,4 +11,4 @@ pub(crate) mod sticky_bottom; pub(crate) mod category_input; pub(crate) mod reoccurrence_input; pub(crate) mod layout; -mod navigation_item; +pub(crate) mod navigation_item; diff --git a/src/components/pages/category_calendar_page.rs b/src/components/pages/category_calendar_page.rs index 6c434ce..7876621 100644 --- a/src/components/pages/category_calendar_page.rs +++ b/src/components/pages/category_calendar_page.rs @@ -1,31 +1,77 @@ -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 chrono::{Datelike, Local, Locale}; 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; +use dioxus_query::prelude::QueryResult; +use crate::components::task_list::TaskList; +use crate::query::QueryValue; +use crate::query::tasks::use_tasks_in_category_query; +use crate::models::task::Task; + +const CALENDAR_LENGTH_DAYS: usize = 366 * 3; #[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(); + let tasks = use_tasks_in_category_query(Category::Calendar { + date: Local::now().date_naive(), + reoccurrence: None, + time: None, + }); + let tasks_query_result = tasks.result(); rsx! { - TaskList { - tasks: tasks, - class: "pb-36" + match tasks_query_result.value() { + QueryResult::Ok(QueryValue::Tasks(tasks)) + | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => { + let today_date = Local::now().date_naive(); + + rsx! { + div { + class: "pt-4 flex flex-col gap-8", + for date_current in today_date.iter_days().take(CALENDAR_LENGTH_DAYS) { + div { + class: "flex flex-col gap-4", + div { + class: "px-8 flex flex-row items-center gap-2 font-bold", + div { + class: "pt-1", + { + date_current + .format_localized( + format!( + "%A %-d. %B{}", + if date_current.year() != today_date.year() {" %Y"} + else {""} + ).as_str(), + Locale::en_US + ) + .to_string() + } + } + } + TaskList { + tasks: tasks.iter().filter(|task| { + if let Category::Calendar { date, .. } = task.category() { + *date == date_current + } else { + panic!("Unexpected category."); + } + }).cloned().collect::>() + } + } + } + } + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + } } } } diff --git a/src/components/pages/category_done_page.rs b/src/components/pages/category_done_page.rs index b524ec8..ad65d31 100644 --- a/src/components/pages/category_done_page.rs +++ b/src/components/pages/category_done_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::Done, } } } diff --git a/src/components/pages/category_inbox_page.rs b/src/components/pages/category_inbox_page.rs index ca592bc..debc408 100644 --- a/src/components/pages/category_inbox_page.rs +++ b/src/components/pages/category_inbox_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::Inbox, } } } diff --git a/src/components/pages/category_long_term_page.rs b/src/components/pages/category_long_term_page.rs index 4cc90de..437de37 100644 --- a/src/components/pages/category_long_term_page.rs +++ b/src/components/pages/category_long_term_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::LongTerm, } } } diff --git a/src/components/pages/category_next_steps_page.rs b/src/components/pages/category_next_steps_page.rs index bfbe17f..c1086f3 100644 --- a/src/components/pages/category_next_steps_page.rs +++ b/src/components/pages/category_next_steps_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::NextSteps, } } } diff --git a/src/components/pages/category_page.rs b/src/components/pages/category_page.rs new file mode 100644 index 0000000..aa0e3dd --- /dev/null +++ b/src/components/pages/category_page.rs @@ -0,0 +1,32 @@ +use crate::components::task_list::TaskList; +use crate::models::category::Category; +use crate::query::tasks::use_tasks_in_category_query; +use crate::query::QueryValue; +use dioxus::core_macro::rsx; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; +use dioxus_query::prelude::QueryResult; + +#[component] +pub(crate) fn CategoryPage(category: Category) -> Element { + let tasks_query = use_tasks_in_category_query(category); + let tasks_query_result = tasks_query.result(); + + match tasks_query_result.value() { + QueryResult::Ok(QueryValue::Tasks(tasks)) + | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! { + TaskList { + tasks: tasks.clone(), + class: "pb-36" + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + } + } +} diff --git a/src/components/pages/category_someday_maybe_page.rs b/src/components/pages/category_someday_maybe_page.rs index bb70a17..7a2070f 100644 --- a/src/components/pages/category_someday_maybe_page.rs +++ b/src/components/pages/category_someday_maybe_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::SomedayMaybe, } } } diff --git a/src/components/pages/category_today_page.rs b/src/components/pages/category_today_page.rs index d01f477..6fb573e 100644 --- a/src/components/pages/category_today_page.rs +++ b/src/components/pages/category_today_page.rs @@ -1,39 +1,158 @@ -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 crate::query::tasks::use_tasks_in_category_query; +use crate::query::QueryValue; +use chrono::{Local, Locale}; use dioxus::prelude::*; +use dioxus_query::prelude::QueryResult; #[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::>(); + let today_date = Local::now().date_naive(); + + let calendar_tasks_query = use_tasks_in_category_query(Category::Calendar { + date: today_date, + reoccurrence: None, + time: None, + }); + let calendar_tasks_query_result = calendar_tasks_query.result(); + + let long_term_tasks_query = use_tasks_in_category_query(Category::LongTerm); + let long_term_tasks_query_result = long_term_tasks_query.result(); rsx! { - TaskList { - tasks: tasks, - class: "pb-36" + div { + class: "pt-4 flex flex-col gap-8", + match long_term_tasks_query_result.value() { + QueryResult::Ok(QueryValue::Tasks(tasks)) + | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! { + div { + class: "flex flex-col gap-4", + div { + class: "px-8 flex flex-row items-center gap-2 font-bold", + i { + class: "fa-solid fa-water text-xl" + } + div { + class: "mt-1", + "Long-term" + } + } + div { + for task in tasks { + div { + key: "{task.id()}", + class: format!( + "px-8 pt-5 {} flex flex-row gap-4", + if task.deadline().is_some() { + "pb-0.5" + } else { + "pb-5" + } + ), + div { + class: "flex flex-col", + div { + class: "mt grow font-medium", + {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()} + } + } + } + } + } + } + } + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + } + } + match calendar_tasks_query_result.value() { + QueryResult::Ok(QueryValue::Tasks(tasks)) + | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => { + let today_tasks = tasks.iter().filter(|task| { + if let Category::Calendar { date, .. } = task.category() { + *date == today_date + } else { + panic!("Unexpected category."); + } + }).cloned().collect::>(); + let overdue_tasks = tasks.iter().filter(|task| { + if let Category::Calendar { date, .. } = task.category() { + *date < today_date + } else { + panic!("Unexpected category."); + } + }).cloned().collect::>(); + + rsx! { + if !overdue_tasks.is_empty() { + div { + class: "flex flex-col gap-4", + div { + class: "px-8 flex flex-row items-center gap-2 font-bold", + i { + class: "fa-solid fa-calendar-xmark text-xl" + } + div { + class: "mt-1", + "Overdue" + } + } + TaskList { + tasks: overdue_tasks, + class: "pb-3" + } + } + } + div { + class: "flex flex-col gap-4", + div { + class: "px-8 flex flex-row items-center gap-2 font-bold", + i { + class: "fa-solid fa-calendar-check text-xl" + } + div { + class: "mt-1", + { + today_date + .format_localized("Today, %A %-d. %B", Locale::en_US) + .to_string() + } + } + } + TaskList { + tasks: today_tasks + } + } + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + } + } } } } diff --git a/src/components/pages/category_trash_page.rs b/src/components/pages/category_trash_page.rs index 538c85d..36b75f7 100644 --- a/src/components/pages/category_trash_page.rs +++ b/src/components/pages/category_trash_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::Trash, } } } diff --git a/src/components/pages/category_waiting_for_page.rs b/src/components/pages/category_waiting_for_page.rs index 3145bfc..062373a 100644 --- a/src/components/pages/category_waiting_for_page.rs +++ b/src/components/pages/category_waiting_for_page.rs @@ -1,27 +1,14 @@ -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; +use crate::components::pages::category_page::CategoryPage; #[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" + CategoryPage { + category: Category::WaitingFor(String::new()), } } } diff --git a/src/components/pages/mod.rs b/src/components/pages/mod.rs index 59bb5ef..9dcc2c3 100644 --- a/src/components/pages/mod.rs +++ b/src/components/pages/mod.rs @@ -9,3 +9,4 @@ pub(crate) mod category_done_page; pub(crate) mod category_trash_page; pub(crate) mod not_found_page; pub(crate) mod projects_page; +pub(crate) mod category_page; diff --git a/src/components/task_form.rs b/src/components/task_form.rs index 981dcd8..99e1879 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -1,4 +1,3 @@ -use std::fmt::Display; use crate::components::category_input::CategoryInput; use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; use crate::models::category::{CalendarTime, Category, Reoccurrence, ReoccurrenceInterval}; @@ -9,6 +8,8 @@ use chrono::{Duration, NaiveDate}; use dioxus::core_macro::{component, rsx}; use dioxus::dioxus_core::Element; use dioxus::prelude::*; +use dioxus_query::prelude::use_query_client; +use crate::query::{QueryErrors, QueryKey, QueryValue}; use crate::route::Route; const REMINDER_OFFSETS: [Option; 17] = [ @@ -52,6 +53,8 @@ pub(crate) fn TaskForm() -> Element { let mut category_calendar_has_time = use_signal(|| false); let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1); + let query_client = use_query_client::(); + rsx! { form { onsubmit: move |event| { @@ -94,6 +97,10 @@ pub(crate) fn TaskForm() -> Element { .as_value().parse::().ok().filter(|&id| id > 0), ); let _ = create_task(new_task).await; + query_client.invalidate_queries(&[ + QueryKey::Tasks, + QueryKey::TasksInCategory(selected_category()) + ]); } }, class: "p-4 flex flex-col gap-4", diff --git a/src/components/task_list.rs b/src/components/task_list.rs index 0612a2e..c9b04cb 100644 --- a/src/components/task_list.rs +++ b/src/components/task_list.rs @@ -1,6 +1,5 @@ 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::*; @@ -9,11 +8,12 @@ use dioxus::prelude::*; 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("")), + class: format!("flex flex-col {}", class.unwrap_or("")), for task in tasks { div { + key: "{task.id()}", class: format!( - "pt-5 {} flex flex-row gap-4", + "px-8 pt-5 {} flex flex-row gap-4", if task.deadline().is_some() { "pb-0.5" } else if let Category::Calendar { time, .. } = task.category() { @@ -32,7 +32,7 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element div { class: "flex flex-col", div { - class: "mt-1 grow", + class: "mt-1 grow font-medium", {task.title()} }, div { diff --git a/src/models/category.rs b/src/models/category.rs index ad35ba6..f20417d 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -11,7 +11,7 @@ use serde_with::DurationSeconds; use std::io::Write; #[serde_with::serde_as] -#[derive(AsExpression, FromSqlRow, Serialize, Deserialize, Clone, Debug)] +#[derive(AsExpression, FromSqlRow, Serialize, Deserialize, Hash, Clone, Debug)] #[diesel(sql_type = Jsonb)] pub enum Category { Inbox, @@ -51,6 +51,8 @@ impl PartialEq for Category { } } +impl Eq for Category {} + impl ToSql for Category { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> diesel::serialize::Result { let json = serde_json::to_string(self)?; @@ -74,14 +76,14 @@ impl FromSql for Category { } } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Hash, Clone, Debug)] pub enum ReoccurrenceInterval { Day, Month, Year, } -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Hash, Clone, Debug)] pub struct Reoccurrence { start_date: NaiveDate, interval: ReoccurrenceInterval, @@ -103,7 +105,7 @@ impl Reoccurrence { } #[serde_with::serde_as] -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Hash, Clone, Debug)] pub struct CalendarTime { time: NaiveTime, #[serde_as(as = "Option>")] diff --git a/src/models/task.rs b/src/models/task.rs index 058874d..0d1d46d 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use validator::Validate;