feat: use Dioxus query for fetching data

This commit is contained in:
Matouš Volf 2024-09-05 17:19:35 +02:00
parent 7c16bc4546
commit 27ba44188e
17 changed files with 290 additions and 169 deletions

View File

@ -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::<QueryValue, QueryErrors, QueryKey>();
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::<Route> {}
}
}

View File

@ -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;

View File

@ -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::<Vec<Task>>()
}
}
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
}
}
}
}

View File

@ -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,
}
}
}

View File

@ -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,
}
}
}

View File

@ -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,
}
}
}

View File

@ -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,
}
}
}

View File

@ -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:?}"
}
}
}
}

View File

@ -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,
}
}
}

View File

@ -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::<Vec<Task>>();
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::<Vec<Task>>();
let overdue_tasks = tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
*date < today_date
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>();
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:?}"
}
}
}
}
}
}

View File

@ -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,
}
}
}

View File

@ -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()),
}
}
}

View File

@ -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;

View File

@ -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<Duration>; 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::<QueryValue, QueryErrors, QueryKey>();
rsx! {
form {
onsubmit: move |event| {
@ -94,6 +97,10 @@ pub(crate) fn TaskForm() -> Element {
.as_value().parse::<i32>().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",

View File

@ -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<Task>, 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<Task>, class: Option<&'static str>) -> Element
div {
class: "flex flex-col",
div {
class: "mt-1 grow",
class: "mt-1 grow font-medium",
{task.title()}
},
div {

View File

@ -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<Jsonb, Pg> 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<Jsonb, Pg> 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<DurationSeconds<i64>>")]

View File

@ -1,3 +1,4 @@
use std::cmp::Ordering;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;