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
-
+
+ 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 */