feat: display a project form upon clicking the create button on the projects page

This commit is contained in:
Matouš Volf 2024-09-05 18:18:03 +02:00
parent 27ba44188e
commit dfefeab69e
13 changed files with 138 additions and 46 deletions

View File

@ -1,18 +1,17 @@
use std::thread::sleep;
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::navigation::Navigation; use crate::components::navigation::Navigation;
use crate::components::project_form::ProjectForm;
use crate::components::task_form::TaskForm; use crate::components::task_form::TaskForm;
use crate::components::task_list::TaskList;
use crate::models::category::Category;
use crate::route::Route; use crate::route::Route;
#[component] #[component]
pub(crate) fn BottomPanel(creating_task: bool) -> Element { pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
let mut expanded = use_signal(|| creating_task); let mut expanded = use_signal(|| display_form());
let navigation_expanded = use_signal(|| false); let navigation_expanded = use_signal(|| false);
let current_route = use_route();
use_effect(use_reactive(&creating_task, move |creating_task| { use_effect(use_reactive(&display_form, move |creating_task| {
if creating_task { if creating_task() {
expanded.set(true); expanded.set(true);
} else { } else {
spawn(async move { spawn(async move {
@ -26,14 +25,30 @@ pub(crate) fn BottomPanel(creating_task: bool) -> Element {
div { div {
class: format!( 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)] {}", "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()) { match (display_form(), current_route, navigation_expanded()) {
(false, false) => "h-[64px]", (false, _, false) => "h-[64px]",
(false, true) => "h-[128px]", (false, _, true) => "h-[128px]",
(true, _) => "h-[448px]", (true, Route::ProjectsPage, _) => "h-[128px]",
(true, _, _) => "h-[448px]",
} }
), ),
if expanded() { if expanded() {
TaskForm {} match current_route {
Route::ProjectsPage => rsx! {
ProjectForm {
on_successful_submit: move |_| {
display_form.set(false);
}
}
},
_ => rsx! {
TaskForm {
on_successful_submit: move |_| {
display_form.set(false);
}
}
}
}
} else { } else {
Navigation { Navigation {
expanded: navigation_expanded, expanded: navigation_expanded,

View File

@ -4,7 +4,7 @@ use crate::models::category::Category;
use crate::route::Route; use crate::route::Route;
#[component] #[component]
pub(crate) fn CreateTaskButton(creating: Signal<bool>) -> Element { pub(crate) fn CreateButton(creating: Signal<bool>) -> Element {
rsx! { rsx! {
button { 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", 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",

View File

@ -1,29 +1,23 @@
use crate::components::bottom_panel::BottomPanel; 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 crate::route::Route;
use chrono::NaiveDate;
use dioxus::core_macro::rsx; use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::components::create_task_button::CreateTaskButton; use crate::components::create_task_button::CreateButton;
use crate::components::sticky_bottom::StickyBottom; use crate::components::sticky_bottom::StickyBottom;
use crate::components::task_form::TaskForm;
use crate::server::tasks::get_tasks_in_category;
#[component] #[component]
pub(crate) fn Layout() -> Element { pub(crate) fn Layout() -> Element {
let creating_task = use_signal(|| false); let display_form = use_signal(|| false);
rsx! { rsx! {
Outlet::<Route> {} Outlet::<Route> {}
StickyBottom { StickyBottom {
CreateTaskButton { CreateButton {
creating: creating_task, creating: display_form,
} }
BottomPanel { BottomPanel {
creating_task: creating_task(), display_form: display_form,
} }
} }
} }

View File

@ -71,7 +71,8 @@ pub(crate) fn CategoryCalendarPage() -> Element {
div { div {
"Errors occurred: {errors:?}" "Errors occurred: {errors:?}"
} }
} },
value => panic!("Unexpected query result: {value:?}")
} }
} }
} }

View File

@ -27,6 +27,7 @@ pub(crate) fn CategoryPage(category: Category) -> Element {
div { div {
"Errors occurred: {errors:?}" "Errors occurred: {errors:?}"
} }
} },
value => panic!("Unexpected query result: {value:?}")
} }
} }

View File

@ -82,7 +82,8 @@ pub(crate) fn CategoryTodayPage() -> Element {
div { div {
"Errors occurred: {errors:?}" "Errors occurred: {errors:?}"
} }
} },
value => panic!("Unexpected query result: {value:?}")
} }
match calendar_tasks_query_result.value() { match calendar_tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks)) QueryResult::Ok(QueryValue::Tasks(tasks))
@ -151,7 +152,8 @@ pub(crate) fn CategoryTodayPage() -> Element {
div { div {
"Errors occurred: {errors:?}" "Errors occurred: {errors:?}"
} }
} },
value => panic!("Unexpected query result: {value:?}")
} }
} }
} }

View File

@ -1,7 +1,36 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
use crate::query::projects::use_projects_query;
use crate::query::QueryValue;
#[component] #[component]
pub(crate) fn ProjectsPage() -> Element { pub(crate) fn ProjectsPage() -> Element {
let projects_query = use_projects_query();
rsx! { rsx! {
match projects_query.result().value() {
QueryResult::Ok(QueryValue::Projects(projects))
| QueryResult::Loading(Some(QueryValue::Projects(projects))) => rsx! {
div {
class: "flex flex-col",
for project in projects {
div {
key: "{project.id()}",
class: "px-8 py-4",
{project.title()}
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
} }
} }

View File

@ -3,9 +3,13 @@ use crate::server::projects::create_project;
use dioxus::core_macro::{component, rsx}; use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue};
#[component] #[component]
pub(crate) fn ProjectForm() -> Element { pub(crate) fn ProjectForm(on_successful_submit: EventHandler<()>) -> Element {
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
rsx! { rsx! {
form { form {
onsubmit: move |event| { onsubmit: move |event| {
@ -14,17 +18,39 @@ pub(crate) fn ProjectForm() -> Element {
event.values().get("title").unwrap().as_value() event.values().get("title").unwrap().as_value()
); );
let _ = create_project(new_project).await; let _ = create_project(new_project).await;
query_client.invalidate_queries(&[
QueryKey::Projects
]);
on_successful_submit.call(());
} }
}, },
class: "p-4 flex flex-col gap-4",
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"
}
}
input { input {
r#type: "text", r#type: "text",
name: "title", name: "title",
required: true, required: true,
placeholder: "title" class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
} }
}
div {
class: "flex flex-row justify-end mt-auto",
button { button {
r#type: "submit", r#type: "submit",
"create" class: "py-2 px-4 bg-zinc-300/50 rounded-lg",
i {
class: "fa-solid fa-floppy-disk"
}
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
use crate::components::category_input::CategoryInput; use crate::components::category_input::CategoryInput;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::models::category::{CalendarTime, Category, Reoccurrence, ReoccurrenceInterval}; use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask; use crate::models::task::NewTask;
use crate::server::projects::get_projects; use crate::server::projects::get_projects;
use crate::server::tasks::create_task; use crate::server::tasks::create_task;
@ -33,11 +33,11 @@ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
]; ];
#[component] #[component]
pub(crate) fn TaskForm() -> Element { pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
let projects = use_server_future(get_projects)?.unwrap().unwrap(); let projects = use_server_future(get_projects)?.unwrap().unwrap();
let route = use_route::<Route>(); let route = use_route::<Route>();
let mut selected_category = use_signal(|| match route { let selected_category = use_signal(|| match route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe, Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()), Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps, Route::CategoryNextStepsPage => Category::NextSteps,
@ -49,7 +49,7 @@ pub(crate) fn TaskForm() -> Element {
Route::CategoryLongTermPage => Category::LongTerm, Route::CategoryLongTermPage => Category::LongTerm,
_ => Category::Inbox, _ => Category::Inbox,
}); });
let mut category_calendar_reoccurrence_interval = use_signal(|| None); let category_calendar_reoccurrence_interval = use_signal(|| None);
let mut category_calendar_has_time = use_signal(|| false); let mut category_calendar_has_time = use_signal(|| false);
let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1); let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1);
@ -101,6 +101,7 @@ pub(crate) fn TaskForm() -> Element {
QueryKey::Tasks, QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category()) QueryKey::TasksInCategory(selected_category())
]); ]);
on_successful_submit.call(());
} }
}, },
class: "p-4 flex flex-col gap-4", class: "p-4 flex flex-col gap-4",
@ -171,7 +172,7 @@ pub(crate) fn TaskForm() -> Element {
} }
}, },
CategoryInput { CategoryInput {
selected_category: selected_category.clone(), selected_category: selected_category,
class: "grow" class: "grow"
} }
} }

View File

@ -6,7 +6,7 @@ use validator::Validate;
const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255; 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::projects)] #[diesel(table_name = crate::schema::projects)]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Project { pub struct Project {

View File

@ -1,9 +1,8 @@
use std::cmp::Ordering; use crate::models::category::Category;
use crate::schema::tasks;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use crate::models::category::Category;
use crate::schema::tasks;
const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255; const TITLE_LENGTH_MAX: u64 = 255;

View File

@ -1,13 +1,16 @@
use crate::errors::error::Error; use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec; use crate::errors::error_vec::ErrorVec;
use crate::models::category::Category; use crate::models::category::Category;
use crate::models::project::Project;
use crate::models::task::Task; use crate::models::task::Task;
pub(crate) mod tasks; pub(crate) mod tasks;
pub(crate) mod projects;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub(crate) enum QueryValue { pub(crate) enum QueryValue {
Tasks(Vec<Task>), Tasks(Vec<Task>),
Projects(Vec<Project>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -19,4 +22,5 @@ pub(crate) enum QueryErrors {
pub(crate) enum QueryKey { pub(crate) enum QueryKey {
Tasks, Tasks,
TasksInCategory(Category), TasksInCategory(Category),
Projects,
} }

20
src/query/projects.rs Normal file
View File

@ -0,0 +1,20 @@
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::projects::get_projects;
use dioxus::prelude::ServerFnError;
use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery};
pub(crate) fn use_projects_query() -> UseQuery<QueryValue, QueryErrors, QueryKey> {
use_get_query([QueryKey::Projects, QueryKey::Tasks], fetch_projects)
}
async fn fetch_projects(keys: Vec<QueryKey>) -> QueryResult<QueryValue, QueryErrors> {
if let Some(QueryKey::Projects) = keys.first() {
match get_projects().await {
Ok(projects) => Ok(QueryValue::Projects(projects)),
Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)),
Err(error) => panic!("Unexpected error: {:?}", error)
}.into()
} else {
panic!("Unexpected query keys: {:?}", keys);
}
}