diff --git a/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml b/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml index ecb9799..cf511a2 100644 --- a/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml +++ b/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b.xml @@ -3,17 +3,9 @@ mdy - 1||-9223372036854775808|c|G -1||10|c|G -1||10|C|G -1||10|T|G -4||-9223372036854775808|c|G -4||10|c|G -4||10|C|G -4||10|T|G - 767 + 785 16.4 - 1723847104 + 1724062819 true ACDT true ACSST false ACST @@ -1412,7 +1404,7 @@ true posixrules 13212||10|C|G 13212||-9223372036854775808|U|G 13212||10|U|G - 767 + 785 16384 app @@ -4831,8 +4823,8 @@ true posixrules standard public schema 1 - 767 - 2024-08-16.22:33:41 + 785 + 2024-08-19.17:09:45 2200 524 pg_database_owner @@ -4873,30 +4865,36 @@ true posixrules 16425 - 762 + 7812app
- + + 16446 + 783 + 2 + app +
+ R void|0s - + 1 regclass|0s - + R trigger|0s - + 1 1 743 varchar(50)|0s 1043 - + CURRENT_TIMESTAMP 1 2 @@ -4904,7 +4902,7 @@ true posixrules timestamp|0s 1114 - + version 1 16393 @@ -4916,14 +4914,14 @@ true posixrules 100 pg_catalog - + 1 16394 1 743 16393 - + nextval('projects_id_seq'::regclass) 1 1 @@ -4932,14 +4930,14 @@ true posixrules 16424 23 - + 1 2 762 text|0s 25 - + id 1 16431 @@ -4948,12 +4946,69 @@ true posixrules 1 403 - + 1 16432 1 762 16431 + + 1 + 1 + 783 + integer|0s + 23 + + + 1 + 2 + 783 + text|0s + 25 + + + 3 + 783 + date|0s + 1082 + + + 1 + 4 + 783 + jsonb|0s + 3802 + + + 5 + 783 + integer|0s + 23 + + + project_id + 1 + 16453 + 783 + 1 + 16425 + + + id + 1 + 16451 + 1 + 783 + 1 + 403 + + + 1 + 16452 + 1 + 783 + 16451 +
\ No newline at end of file diff --git a/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b/storage_v2/_src_/database/todo_baggins.NgsZOg/schema/public.abK9xQ.meta b/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b/storage_v2/_src_/database/todo_baggins.NgsZOg/schema/public.abK9xQ.meta index c523994..ba67131 100644 --- a/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b/storage_v2/_src_/database/todo_baggins.NgsZOg/schema/public.abK9xQ.meta +++ b/.idea/dataSources/1658668c-c2b8-426d-a22f-16fbad9eff0b/storage_v2/_src_/database/todo_baggins.NgsZOg/schema/public.abK9xQ.meta @@ -1,2 +1,2 @@ #n:public -! [767, 0, null, null, -2147483648, -2147483648] +! [785, 0, null, null, -2147483648, -2147483648] diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 6335f8e..7ddfc9e 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,8 +2,8 @@ - - + + diff --git a/Cargo.lock b/Cargo.lock index 53679ab..7fcda7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "async-trait", "axum-core", "axum-macros", - "base64", + "base64 0.21.7", "bytes", "futures-util", "http 1.1.0", @@ -211,6 +211,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bincode" version = "1.3.3" @@ -335,6 +341,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets", ] @@ -499,7 +506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -511,6 +518,16 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "diesel" version = "2.2.2" @@ -519,9 +536,11 @@ checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94" dependencies = [ "bitflags", "byteorder", + "chrono", "diesel_derives", "itoa", "pq-sys", + "serde_json", ] [[package]] @@ -647,7 +666,7 @@ dependencies = [ "anymap", "async-trait", "axum", - "base64", + "base64 0.21.7", "bytes", "ciborium", "dioxus-cli-config", @@ -1400,6 +1419,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1428,6 +1453,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.12" @@ -1564,6 +1595,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.4.0" @@ -1571,7 +1613,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1580,7 +1623,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", "parking_lot", ] @@ -1695,7 +1738,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1780,6 +1823,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -1861,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.4.0", ] [[package]] @@ -1907,6 +1956,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -2199,6 +2254,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.4.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "server_fn" version = "0.6.14" @@ -2442,6 +2527,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2467,11 +2583,16 @@ checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" name = "todo-baggins" version = "0.1.0" dependencies = [ + "chrono", "diesel", "dioxus", "dioxus-logger", "dotenvy", "serde", + "serde_json", + "serde_with", + "tracing", + "tracing-wasm", "validator", ] @@ -2538,7 +2659,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "hashbrown", + "hashbrown 0.14.5", "pin-project-lite", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 9eabee1..ac211f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -diesel = { version = "2.2.2", features = ["postgres"] } +chrono = { version = "0.4.38", features = ["serde"] } +diesel = { version = "2.2.2", features = ["chrono", "postgres", "postgres_backend", "serde_json"] } dioxus = { version = "0.5", features = ["fullstack", "router"] } @@ -16,6 +17,10 @@ dioxus-logger = "0.5.1" dotenvy = "0.15.7" serde = "1.0.208" validator = { version = "0.18.1", features = ["derive"] } +serde_json = "1.0.125" +tracing = "0.1.40" +tracing-wasm = "0.2.1" +serde_with = { version = "3.9.0", features = ["chrono_0_4"] } [features] default = [] diff --git a/migrations/2024-08-19-105140_create_tasks/down.sql b/migrations/2024-08-19-105140_create_tasks/down.sql new file mode 100644 index 0000000..90c7744 --- /dev/null +++ b/migrations/2024-08-19-105140_create_tasks/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE IF EXISTS "tasks"; diff --git a/migrations/2024-08-19-105140_create_tasks/up.sql b/migrations/2024-08-19-105140_create_tasks/up.sql new file mode 100644 index 0000000..d9e7b2c --- /dev/null +++ b/migrations/2024-08-19-105140_create_tasks/up.sql @@ -0,0 +1,11 @@ +-- Your SQL goes here + +CREATE TABLE "tasks"( + "id" SERIAL NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "deadline" DATE, + "category" JSONB NOT NULL, + "project_id" INT4, + FOREIGN KEY ("project_id") REFERENCES "projects"("id") +); + diff --git a/src/components/app.rs b/src/components/app.rs index ac00f95..0ccd3a2 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -6,6 +6,9 @@ use dioxus::prelude::*; #[component] pub(crate) fn App() -> Element { rsx! { - Router:: {} + div { + class: "min-h-screen text-white bg-neutral-800", + Router:: {} + } } } diff --git a/src/components/home.rs b/src/components/home.rs index 12b1ddc..827fad4 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -2,10 +2,12 @@ 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/mod.rs b/src/components/mod.rs index cced83e..c632a34 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod app; pub(crate) mod home; pub(crate) mod project_form; +pub(crate) mod task_form; diff --git a/src/components/project_form.rs b/src/components/project_form.rs index 036e0d7..6d64abf 100644 --- a/src/components/project_form.rs +++ b/src/components/project_form.rs @@ -19,6 +19,7 @@ pub(crate) fn ProjectForm() -> Element { input { r#type: "text", name: "title", + required: true, placeholder: "title" } button { diff --git a/src/components/task_form.rs b/src/components/task_form.rs new file mode 100644 index 0000000..a280437 --- /dev/null +++ b/src/components/task_form.rs @@ -0,0 +1,221 @@ +use chrono::Duration; +use crate::models::category::{CalendarTime, Category}; +use crate::models::task::NewTask; +use crate::server::projects::get_projects; +use crate::server::tasks::create_task; +use dioxus::core_macro::{component, rsx}; +use dioxus::dioxus_core::Element; +use dioxus::prelude::*; + +#[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); + + 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() + ] { + Category::WaitingFor(_) => Category::WaitingFor( + event.values().get("category_waiting_for").unwrap() + .as_value() + ), + 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") + .unwrap().as_value().parse().unwrap() + ) + ), + time: event.values().get("category_calendar_time").unwrap() + .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() + ) + ) + ) + ) + }, + category => category.clone() + }, + event.values().get("project_id").unwrap() + .as_value().parse::().ok().filter(|&id| id > 0), + ); + let _ = create_task(new_task).await; + } + }, + 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", + }, + }, + Category::Calendar { .. } => rsx !{ + input { + r#type: "date", + name: "category_calendar_date", + required: true, + class: "p-2 bg-neutral-700 rounded", + }, + 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()); + } + }, + label { + r#for: "category_calendar_is_reoccurring", + " is reoccurring" + } + }, + if category_calendar_is_reoccurring() { + input { + r#type: "number", + name: "category_calendar_reoccurance_interval", + required: true, + min: 1, + placeholder: "reoccurance interval (days)", + class: "p-2 bg-neutral-700 rounded", + } + }, + 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()); + } + }, + if category_calendar_has_time() { + div { + input { + r#type: "checkbox", + name: "category_calendar_has_reminder", + value: 0, + id: "category_calendar_has_reminder", + onchange: move |event| { + category_calendar_has_reminder.set(event.checked()); + } + }, + label { + r#for: "category_calendar_has_reminder", + " set a reminder" + } + } + } + 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()} + } + } + }, + button { + r#type: "submit", + "create" + } + } +} +} diff --git a/src/errors/error.rs b/src/errors/error.rs index dc235c5..a26a41b 100644 --- a/src/errors/error.rs +++ b/src/errors/error.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::str::FromStr; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum Error { ServerInternal, } diff --git a/src/errors/error_vec.rs b/src/errors/error_vec.rs index 557b338..7ace73f 100644 --- a/src/errors/error_vec.rs +++ b/src/errors/error_vec.rs @@ -1,7 +1,9 @@ use std::fmt::Display; use std::str::FromStr; +use serde::Deserialize; +use serde_with::serde_derive::Serialize; -#[derive(Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ErrorVec { errors: Vec, } @@ -37,7 +39,7 @@ impl Display for ErrorVec { impl FromStr for ErrorVec { type Err = (); - fn from_str(s: &str) -> Result { + fn from_str(_: &str) -> Result { Ok(ErrorVec { errors: Vec::new() }) } } diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 625611e..a0ca9ed 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod error; pub(crate) mod error_vec; pub(crate) mod project_create_error; +pub(crate) mod task_create_error; diff --git a/src/errors/project_create_error.rs b/src/errors/project_create_error.rs index d8fad8d..c3dead5 100644 --- a/src/errors/project_create_error.rs +++ b/src/errors/project_create_error.rs @@ -12,8 +12,8 @@ pub enum ProjectCreateError { } impl From for ErrorVec { - fn from(e: ValidationErrors) -> Self { - e.errors() + fn from(validation_errors: ValidationErrors) -> Self { + validation_errors.errors() .iter() .flat_map(|(&field, error_kind)| match field { "title" => match error_kind { @@ -22,30 +22,30 @@ impl From for ErrorVec { .map(|validation_error| validation_error.code.as_ref()) .map(|code| match code { "title_length" => ProjectCreateError::TitleLengthInvalid, - _ => panic!("unexpected validation error code: {code}"), + _ => panic!("Unexpected validation error code: `{code}`."), }) .collect::>(), - _ => panic!("unexpected validation error kind"), + _ => panic!("Unexpected validation error kind."), }, - _ => panic!("unexpected validation field name: {field}"), + _ => panic!("Unexpected validation field name: `{field}`."), }) .collect::>() .into() } } -// has to be implemented for Dioxus server functions +// Has to be implemented for Dioxus server functions. impl Display for ProjectCreateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } -// has to be implemented for Dioxus server functions +// Has to be implemented for Dioxus server functions. impl FromStr for ProjectCreateError { type Err = (); fn from_str(_: &str) -> Result { - Ok(ProjectCreateError::TitleLengthInvalid) + Ok(ProjectCreateError::Error(Error::ServerInternal)) } } diff --git a/src/errors/task_create_error.rs b/src/errors/task_create_error.rs new file mode 100644 index 0000000..649163b --- /dev/null +++ b/src/errors/task_create_error.rs @@ -0,0 +1,52 @@ +use crate::errors::error::Error; +use crate::errors::error_vec::ErrorVec; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::str::FromStr; +use validator::{ValidationErrors, ValidationErrorsKind}; + +#[derive(Serialize, Deserialize, Debug)] +pub enum TaskCreateError { + TitleLengthInvalid, + ProjectNotFound, + Error(Error), +} + +impl From for ErrorVec { + fn from(validation_errors: ValidationErrors) -> Self { + validation_errors.errors() + .iter() + .flat_map(|(&field, error_kind)| match field { + "title" => match error_kind { + ValidationErrorsKind::Field(validation_errors) => validation_errors + .iter() + .map(|validation_error| validation_error.code.as_ref()) + .map(|code| match code { + "title_length" => TaskCreateError::TitleLengthInvalid, + _ => panic!("Unexpected validation error code: `{code}`."), + }) + .collect::>(), + _ => panic!("Unexpected validation error kind."), + }, + _ => panic!("Unexpected validation field name: `{field}`."), + }) + .collect::>() + .into() + } +} + +// Has to be implemented for Dioxus server functions. +impl Display for TaskCreateError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +// Has to be implemented for Dioxus server functions. +impl FromStr for TaskCreateError { + type Err = (); + + fn from_str(_: &str) -> Result { + Ok(TaskCreateError::Error(Error::ServerInternal)) + } +} diff --git a/src/models/category.rs b/src/models/category.rs new file mode 100644 index 0000000..c6ca42c --- /dev/null +++ b/src/models/category.rs @@ -0,0 +1,65 @@ +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 serde::{Deserialize, Serialize}; +use serde_with::DurationSeconds; +use std::io::Write; + +#[serde_with::serde_as] +#[derive(AsExpression, FromSqlRow, Serialize, Deserialize, Clone, Debug)] +#[diesel(sql_type = Jsonb)] +pub enum Category { + Inbox, + SomedayMaybe, + WaitingFor(String), + NextSteps, + Calendar { + date: NaiveDate, + #[serde_as(as = "Option>")] + reoccurance_interval: Option, + time: Option, + }, + LongTerm, + Done, + Trash, +} + +#[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 } + } +} + +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)?; + + // Prepend the JSONB version byte. + out.write_all(&[1])?; + out.write_all(json.as_bytes())?; + + Ok(diesel::serialize::IsNull::No) + } +} + +impl FromSql for Category { + fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { + let bytes = bytes.as_bytes(); + if bytes.is_empty() { + return Err("Unexpected empty bytes (missing the JSONB version number).".into()); + } + let str = std::str::from_utf8(&bytes[1..])?; + serde_json::from_str(str).map_err(Into::into) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index dfc721a..9f449ce 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,3 @@ pub(crate) mod project; +pub(crate) mod category; +pub(crate) mod task; diff --git a/src/models/project.rs b/src/models/project.rs index 5d545e9..00aec2e 100644 --- a/src/models/project.rs +++ b/src/models/project.rs @@ -6,7 +6,7 @@ use validator::Validate; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, Debug)] +#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)] #[diesel(table_name = crate::schema::projects)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Project { diff --git a/src/models/task.rs b/src/models/task.rs new file mode 100644 index 0000000..d67256a --- /dev/null +++ b/src/models/task.rs @@ -0,0 +1,60 @@ +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; + +#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)] +#[diesel(table_name = crate::schema::tasks)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Task { + id: i32, + title: String, + deadline: Option, + category: Category, + project_id: Option, +} + +impl Task { + pub fn id(&self) -> i32 { + self.id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn deadline(&self) -> Option { + self.deadline + } + + pub fn category(&self) -> &Category { + &self.category + } + + pub fn project_id(&self) -> Option { + self.project_id + } +} + +#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)] +#[diesel(table_name = tasks)] +pub struct NewTask { + #[validate(length(min = "TITLE_LENGTH_MIN", max = "TITLE_LENGTH_MAX", code = "title_length"))] + pub title: String, + pub deadline: Option, + pub category: Category, + pub project_id: Option, +} + +impl NewTask { + pub fn new( + title: String, deadline: Option, + category: Category, project_id: Option, + ) -> Self { + Self { title, deadline, category, project_id } + } +} diff --git a/src/schema/mod.rs b/src/schema/mod.rs index 6ad8716..a87d07d 100644 --- a/src/schema/mod.rs +++ b/src/schema/mod.rs @@ -6,3 +6,20 @@ diesel::table! { title -> Text, } } + +diesel::table! { + tasks (id) { + id -> Int4, + title -> Text, + deadline -> Nullable, + category -> Jsonb, + project_id -> Nullable, + } +} + +diesel::joinable!(tasks -> projects (project_id)); + +diesel::allow_tables_to_appear_in_same_query!( + projects, + tasks, +); diff --git a/src/server/mod.rs b/src/server/mod.rs index 56bd601..86a456c 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,2 +1,3 @@ mod database_connection; pub(crate) mod projects; +pub(crate) mod tasks; diff --git a/src/server/projects.rs b/src/server/projects.rs index 6d459d2..9644cc8 100644 --- a/src/server/projects.rs +++ b/src/server/projects.rs @@ -3,23 +3,21 @@ use crate::errors::error_vec::ErrorVec; use crate::errors::project_create_error::ProjectCreateError; use crate::models::project::{NewProject, Project}; use crate::server::database_connection::establish_database_connection; -use diesel::{RunQueryDsl, SelectableHelper}; +use diesel::{QueryDsl, RunQueryDsl, SelectableHelper}; use dioxus::prelude::*; use validator::Validate; #[server] -pub(crate) async fn create_project( - new_project: NewProject, -) -> Result>> { +pub(crate) async fn create_project(new_project: NewProject) + -> Result>> { use crate::schema::projects; - new_project - .validate() + new_project.validate() .map_err::, _>(|errors| errors.into())?; let mut connection = establish_database_connection() .map_err::, _>( - |_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into() + |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() )?; let new_project = diesel::insert_into(projects::table) @@ -27,8 +25,28 @@ pub(crate) async fn create_project( .returning(Project::as_returning()) .get_result(&mut connection) .map_err::, _>( - |_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into() + |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() )?; Ok(new_project) } + +#[server] +pub(crate) async fn get_projects() + -> Result, ServerFnError>> { + use crate::schema::projects::dsl::*; + + let mut connection = establish_database_connection() + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + let results = projects + .select(Project::as_select()) + .load::(&mut connection) + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + Ok(results) +} diff --git a/src/server/tasks.rs b/src/server/tasks.rs new file mode 100644 index 0000000..0b7c086 --- /dev/null +++ b/src/server/tasks.rs @@ -0,0 +1,45 @@ +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 dioxus::prelude::*; +use validator::Validate; +use crate::errors::task_create_error::TaskCreateError; + +#[server] +pub(crate) async fn create_task(new_task: NewTask) + -> Result>> { + use crate::schema::tasks; + + new_task.validate() + .map_err::, _>(|errors| errors.into())?; + + let mut connection = establish_database_connection() + .map_err::, _>( + |_| vec![TaskCreateError::Error(Error::ServerInternal)].into() + )?; + + let new_task = diesel::insert_into(tasks::table) + .values(&new_task) + .returning(Task::as_returning()) + .get_result(&mut connection) + .map_err::, _>(|error| { + let error = match error { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::ForeignKeyViolation, info + ) => { + match info.constraint_name() { + Some("tasks_project_id_fkey") => TaskCreateError::ProjectNotFound, + _ => TaskCreateError::Error(Error::ServerInternal) + } + }, + _ => { + TaskCreateError::Error(Error::ServerInternal) + } + }; + vec![error].into() + })?; + + Ok(new_task) +} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 24a8f96..4f70d83 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -1,8 +1,15 @@ /* stylelint-disable */ + /* noinspection CssInvalidAtRule */ @tailwind base; /* noinspection CssInvalidAtRule */ @tailwind components; /* noinspection CssInvalidAtRule */ @tailwind utilities; + +html, body, #main { + /* noinspection CssInvalidAtRule */ + @apply min-h-screen; +} + /* stylelint-enable */