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 270e06de46
commit fe1837d6a1
13 changed files with 138 additions and 46 deletions

View File

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

View File

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

View File

@ -1,29 +1,23 @@
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::create_task_button::CreateButton;
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);
let display_form = use_signal(|| false);
rsx! {
Outlet::<Route> {}
StickyBottom {
CreateTaskButton {
creating: creating_task,
CreateButton {
creating: display_form,
}
BottomPanel {
creating_task: creating_task(),
display_form: display_form,
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,36 @@
use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
use crate::query::projects::use_projects_query;
use crate::query::QueryValue;
#[component]
pub(crate) fn ProjectsPage() -> Element {
let projects_query = use_projects_query();
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::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue};
#[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! {
form {
onsubmit: move |event| {
@ -14,17 +18,39 @@ pub(crate) fn ProjectForm() -> Element {
event.values().get("title").unwrap().as_value()
);
let _ = create_project(new_project).await;
query_client.invalidate_queries(&[
QueryKey::Projects
]);
on_successful_submit.call(());
}
},
input {
r#type: "text",
name: "title",
required: true,
placeholder: "title"
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 {
r#type: "text",
name: "title",
required: true,
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
}
}
button {
r#type: "submit",
"create"
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"
}
}
}
}
}

View File

@ -1,6 +1,6 @@
use crate::components::category_input::CategoryInput;
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::server::projects::get_projects;
use crate::server::tasks::create_task;
@ -33,11 +33,11 @@ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
];
#[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 route = use_route::<Route>();
let mut selected_category = use_signal(|| match route {
let selected_category = use_signal(|| match route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps,
@ -49,7 +49,7 @@ pub(crate) fn TaskForm() -> Element {
Route::CategoryLongTermPage => Category::LongTerm,
_ => 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_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1);
@ -101,6 +101,7 @@ pub(crate) fn TaskForm() -> Element {
QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category())
]);
on_successful_submit.call(());
}
},
class: "p-4 flex flex-col gap-4",
@ -171,7 +172,7 @@ pub(crate) fn TaskForm() -> Element {
}
},
CategoryInput {
selected_category: selected_category.clone(),
selected_category: selected_category,
class: "grow"
}
}

View File

@ -6,7 +6,7 @@ use validator::Validate;
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::projects)]
#[diesel(check_for_backend(diesel::pg::Pg))]
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 serde::{Deserialize, Serialize};
use validator::Validate;
use crate::models::category::Category;
use crate::schema::tasks;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;

View File

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