feat: ability to create a task (#14)

This commit is contained in:
Matouš Volf 2024-08-22 23:40:47 +02:00 committed by GitHub
commit 86710dd3e1
26 changed files with 751 additions and 58 deletions

View File

@ -3,17 +3,9 @@
<database-model serializer="dbm" dbms="POSTGRES" family-id="POSTGRES" format-version="4.53"> <database-model serializer="dbm" dbms="POSTGRES" family-id="POSTGRES" format-version="4.53">
<root id="1"> <root id="1">
<DateStyle>mdy</DateStyle> <DateStyle>mdy</DateStyle>
<Grants>1||-9223372036854775808|c|G <IntrospectionStateNumber>785</IntrospectionStateNumber>
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</Grants>
<IntrospectionStateNumber>767</IntrospectionStateNumber>
<ServerVersion>16.4</ServerVersion> <ServerVersion>16.4</ServerVersion>
<StartupTime>1723847104</StartupTime> <StartupTime>1724062819</StartupTime>
<TimeZones>true ACDT <TimeZones>true ACDT
true ACSST true ACSST
false ACST false ACST
@ -1412,7 +1404,7 @@ true posixrules
13212||10|C|G 13212||10|C|G
13212||-9223372036854775808|U|G 13212||-9223372036854775808|U|G
13212||10|U|G</Grants> 13212||10|U|G</Grants>
<IntrospectionStateNumber>767</IntrospectionStateNumber> <IntrospectionStateNumber>785</IntrospectionStateNumber>
<ObjectId>16384</ObjectId> <ObjectId>16384</ObjectId>
<OwnerName>app</OwnerName> <OwnerName>app</OwnerName>
</database> </database>
@ -4831,8 +4823,8 @@ true posixrules
<schema id="264" parent="3" name="public"> <schema id="264" parent="3" name="public">
<Comment>standard public schema</Comment> <Comment>standard public schema</Comment>
<Current>1</Current> <Current>1</Current>
<IntrospectionStateNumber>767</IntrospectionStateNumber> <IntrospectionStateNumber>785</IntrospectionStateNumber>
<LastIntrospectionLocalTimestamp>2024-08-16.22:33:41</LastIntrospectionLocalTimestamp> <LastIntrospectionLocalTimestamp>2024-08-19.17:09:45</LastIntrospectionLocalTimestamp>
<ObjectId>2200</ObjectId> <ObjectId>2200</ObjectId>
<StateNumber>524</StateNumber> <StateNumber>524</StateNumber>
<OwnerName>pg_database_owner</OwnerName> <OwnerName>pg_database_owner</OwnerName>
@ -4873,30 +4865,36 @@ true posixrules
</table> </table>
<table id="269" parent="264" name="projects"> <table id="269" parent="264" name="projects">
<ObjectId>16425</ObjectId> <ObjectId>16425</ObjectId>
<StateNumber>762</StateNumber> <StateNumber>781</StateNumber>
<AccessMethodId>2</AccessMethodId> <AccessMethodId>2</AccessMethodId>
<OwnerName>app</OwnerName> <OwnerName>app</OwnerName>
</table> </table>
<argument id="270" parent="265"> <table id="270" parent="264" name="tasks">
<ObjectId>16446</ObjectId>
<StateNumber>783</StateNumber>
<AccessMethodId>2</AccessMethodId>
<OwnerName>app</OwnerName>
</table>
<argument id="271" parent="265">
<ArgumentDirection>R</ArgumentDirection> <ArgumentDirection>R</ArgumentDirection>
<StoredType>void|0s</StoredType> <StoredType>void|0s</StoredType>
</argument> </argument>
<argument id="271" parent="265" name="_tbl"> <argument id="272" parent="265" name="_tbl">
<Position>1</Position> <Position>1</Position>
<StoredType>regclass|0s</StoredType> <StoredType>regclass|0s</StoredType>
</argument> </argument>
<argument id="272" parent="266"> <argument id="273" parent="266">
<ArgumentDirection>R</ArgumentDirection> <ArgumentDirection>R</ArgumentDirection>
<StoredType>trigger|0s</StoredType> <StoredType>trigger|0s</StoredType>
</argument> </argument>
<column id="273" parent="268" name="version"> <column id="274" parent="268" name="version">
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
<StateNumber>743</StateNumber> <StateNumber>743</StateNumber>
<StoredType>varchar(50)|0s</StoredType> <StoredType>varchar(50)|0s</StoredType>
<TypeId>1043</TypeId> <TypeId>1043</TypeId>
</column> </column>
<column id="274" parent="268" name="run_on"> <column id="275" parent="268" name="run_on">
<DefaultExpression>CURRENT_TIMESTAMP</DefaultExpression> <DefaultExpression>CURRENT_TIMESTAMP</DefaultExpression>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
@ -4904,7 +4902,7 @@ true posixrules
<StoredType>timestamp|0s</StoredType> <StoredType>timestamp|0s</StoredType>
<TypeId>1114</TypeId> <TypeId>1114</TypeId>
</column> </column>
<index id="275" parent="268" name="__diesel_schema_migrations_pkey"> <index id="276" parent="268" name="__diesel_schema_migrations_pkey">
<ColNames>version</ColNames> <ColNames>version</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ObjectId>16393</ObjectId> <ObjectId>16393</ObjectId>
@ -4916,14 +4914,14 @@ true posixrules
<CollationIds>100</CollationIds> <CollationIds>100</CollationIds>
<CollationParentNames>pg_catalog</CollationParentNames> <CollationParentNames>pg_catalog</CollationParentNames>
</index> </index>
<key id="276" parent="268" name="__diesel_schema_migrations_pkey"> <key id="277" parent="268" name="__diesel_schema_migrations_pkey">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ObjectId>16394</ObjectId> <ObjectId>16394</ObjectId>
<Primary>1</Primary> <Primary>1</Primary>
<StateNumber>743</StateNumber> <StateNumber>743</StateNumber>
<UnderlyingIndexId>16393</UnderlyingIndexId> <UnderlyingIndexId>16393</UnderlyingIndexId>
</key> </key>
<column id="277" parent="269" name="id"> <column id="278" parent="269" name="id">
<DefaultExpression>nextval(&apos;projects_id_seq&apos;::regclass)</DefaultExpression> <DefaultExpression>nextval(&apos;projects_id_seq&apos;::regclass)</DefaultExpression>
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>1</Position> <Position>1</Position>
@ -4932,14 +4930,14 @@ true posixrules
<SequenceId>16424</SequenceId> <SequenceId>16424</SequenceId>
<TypeId>23</TypeId> <TypeId>23</TypeId>
</column> </column>
<column id="278" parent="269" name="title"> <column id="279" parent="269" name="title">
<NotNull>1</NotNull> <NotNull>1</NotNull>
<Position>2</Position> <Position>2</Position>
<StateNumber>762</StateNumber> <StateNumber>762</StateNumber>
<StoredType>text|0s</StoredType> <StoredType>text|0s</StoredType>
<TypeId>25</TypeId> <TypeId>25</TypeId>
</column> </column>
<index id="279" parent="269" name="projects_pkey"> <index id="280" parent="269" name="projects_pkey">
<ColNames>id</ColNames> <ColNames>id</ColNames>
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ObjectId>16431</ObjectId> <ObjectId>16431</ObjectId>
@ -4948,12 +4946,69 @@ true posixrules
<Unique>1</Unique> <Unique>1</Unique>
<AccessMethodId>403</AccessMethodId> <AccessMethodId>403</AccessMethodId>
</index> </index>
<key id="280" parent="269" name="projects_pkey"> <key id="281" parent="269" name="projects_pkey">
<NameSurrogate>1</NameSurrogate> <NameSurrogate>1</NameSurrogate>
<ObjectId>16432</ObjectId> <ObjectId>16432</ObjectId>
<Primary>1</Primary> <Primary>1</Primary>
<StateNumber>762</StateNumber> <StateNumber>762</StateNumber>
<UnderlyingIndexId>16431</UnderlyingIndexId> <UnderlyingIndexId>16431</UnderlyingIndexId>
</key> </key>
<column id="282" parent="270" name="id">
<NotNull>1</NotNull>
<Position>1</Position>
<StateNumber>783</StateNumber>
<StoredType>integer|0s</StoredType>
<TypeId>23</TypeId>
</column>
<column id="283" parent="270" name="title">
<NotNull>1</NotNull>
<Position>2</Position>
<StateNumber>783</StateNumber>
<StoredType>text|0s</StoredType>
<TypeId>25</TypeId>
</column>
<column id="284" parent="270" name="deadline">
<Position>3</Position>
<StateNumber>783</StateNumber>
<StoredType>date|0s</StoredType>
<TypeId>1082</TypeId>
</column>
<column id="285" parent="270" name="category">
<NotNull>1</NotNull>
<Position>4</Position>
<StateNumber>783</StateNumber>
<StoredType>jsonb|0s</StoredType>
<TypeId>3802</TypeId>
</column>
<column id="286" parent="270" name="project_id">
<Position>5</Position>
<StateNumber>783</StateNumber>
<StoredType>integer|0s</StoredType>
<TypeId>23</TypeId>
</column>
<foreign-key id="287" parent="270" name="tasks_project_id_fkey">
<ColNames>project_id</ColNames>
<NameSurrogate>1</NameSurrogate>
<ObjectId>16453</ObjectId>
<StateNumber>783</StateNumber>
<RefKeyColPositions>1</RefKeyColPositions>
<RefTableId>16425</RefTableId>
</foreign-key>
<index id="288" parent="270" name="tasks_pkey">
<ColNames>id</ColNames>
<NameSurrogate>1</NameSurrogate>
<ObjectId>16451</ObjectId>
<Primary>1</Primary>
<StateNumber>783</StateNumber>
<Unique>1</Unique>
<AccessMethodId>403</AccessMethodId>
</index>
<key id="289" parent="270" name="tasks_pkey">
<NameSurrogate>1</NameSurrogate>
<ObjectId>16452</ObjectId>
<Primary>1</Primary>
<StateNumber>783</StateNumber>
<UnderlyingIndexId>16451</UnderlyingIndexId>
</key>
</database-model> </database-model>
</dataSource> </dataSource>

View File

@ -1,2 +1,2 @@
#n:public #n:public
!<md> [767, 0, null, null, -2147483648, -2147483648] !<md> [785, 0, null, null, -2147483648, -2147483648]

4
.idea/vcs.xml generated
View File

@ -2,8 +2,8 @@
<project version="4"> <project version="4">
<component name="CommitMessageInspectionProfile"> <component name="CommitMessageInspectionProfile">
<profile version="1.0"> <profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">

137
Cargo.lock generated
View File

@ -127,7 +127,7 @@ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"axum-macros", "axum-macros",
"base64", "base64 0.21.7",
"bytes", "bytes",
"futures-util", "futures-util",
"http 1.1.0", "http 1.1.0",
@ -211,6 +211,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "1.3.3" version = "1.3.3"
@ -335,6 +341,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-targets", "windows-targets",
] ]
@ -499,7 +506,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"hashbrown", "hashbrown 0.14.5",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core",
@ -511,6 +518,16 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 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]] [[package]]
name = "diesel" name = "diesel"
version = "2.2.2" version = "2.2.2"
@ -519,9 +536,11 @@ checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys", "pq-sys",
"serde_json",
] ]
[[package]] [[package]]
@ -647,7 +666,7 @@ dependencies = [
"anymap", "anymap",
"async-trait", "async-trait",
"axum", "axum",
"base64", "base64 0.21.7",
"bytes", "bytes",
"ciborium", "ciborium",
"dioxus-cli-config", "dioxus-cli-config",
@ -1400,6 +1419,12 @@ dependencies = [
"crunchy", "crunchy",
] ]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@ -1428,6 +1453,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -1564,6 +1595,17 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.4.0"
@ -1571,7 +1613,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown 0.14.5",
"serde",
] ]
[[package]] [[package]]
@ -1580,7 +1623,7 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef" checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.14.5",
"parking_lot", "parking_lot",
] ]
@ -1695,7 +1738,7 @@ version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [ dependencies = [
"hashbrown", "hashbrown 0.14.5",
] ]
[[package]] [[package]]
@ -1780,6 +1823,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1861,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [ dependencies = [
"fixedbitset", "fixedbitset",
"indexmap", "indexmap 2.4.0",
] ]
[[package]] [[package]]
@ -1907,6 +1956,12 @@ dependencies = [
"futures-io", "futures-io",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.20"
@ -2199,6 +2254,36 @@ dependencies = [
"serde", "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]] [[package]]
name = "server_fn" name = "server_fn"
version = "0.6.14" version = "0.6.14"
@ -2442,6 +2527,37 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.0"
@ -2467,11 +2583,16 @@ checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
name = "todo-baggins" name = "todo-baggins"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"diesel", "diesel",
"dioxus", "dioxus",
"dioxus-logger", "dioxus-logger",
"dotenvy", "dotenvy",
"serde", "serde",
"serde_json",
"serde_with",
"tracing",
"tracing-wasm",
"validator", "validator",
] ]
@ -2538,7 +2659,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"hashbrown", "hashbrown 0.14.5",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
] ]

View File

@ -7,7 +7,8 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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"] } dioxus = { version = "0.5", features = ["fullstack", "router"] }
@ -16,6 +17,10 @@ dioxus-logger = "0.5.1"
dotenvy = "0.15.7" dotenvy = "0.15.7"
serde = "1.0.208" serde = "1.0.208"
validator = { version = "0.18.1", features = ["derive"] } 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] [features]
default = [] default = []

View File

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS "tasks";

View File

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

View File

@ -6,6 +6,9 @@ use dioxus::prelude::*;
#[component] #[component]
pub(crate) fn App() -> Element { pub(crate) fn App() -> Element {
rsx! { rsx! {
div {
class: "min-h-screen text-white bg-neutral-800",
Router::<Route> {} Router::<Route> {}
} }
} }
}

View File

@ -2,10 +2,12 @@ use crate::components::project_form::ProjectForm;
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::task_form::TaskForm;
#[component] #[component]
pub(crate) fn Home() -> Element { pub(crate) fn Home() -> Element {
rsx! { rsx! {
ProjectForm {} ProjectForm {}
TaskForm {}
} }
} }

View File

@ -1,3 +1,4 @@
pub(crate) mod app; pub(crate) mod app;
pub(crate) mod home; pub(crate) mod home;
pub(crate) mod project_form; pub(crate) mod project_form;
pub(crate) mod task_form;

View File

@ -19,6 +19,7 @@ pub(crate) fn ProjectForm() -> Element {
input { input {
r#type: "text", r#type: "text",
name: "title", name: "title",
required: true,
placeholder: "title" placeholder: "title"
} }
button { button {

221
src/components/task_form.rs Normal file
View File

@ -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::<usize>(|| 0);
let mut category_calendar_is_reoccurring = use_signal::<bool>(|| false);
let mut category_calendar_has_time = use_signal::<bool>(|| false);
let mut category_calendar_has_reminder = use_signal::<bool>(|| 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::<usize>().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::<i32>().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"
}
}
}
}

View File

@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum Error { pub enum Error {
ServerInternal, ServerInternal,
} }

View File

@ -1,7 +1,9 @@
use std::fmt::Display; use std::fmt::Display;
use std::str::FromStr; use std::str::FromStr;
use serde::Deserialize;
use serde_with::serde_derive::Serialize;
#[derive(Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ErrorVec<T> { pub struct ErrorVec<T> {
errors: Vec<T>, errors: Vec<T>,
} }
@ -37,7 +39,7 @@ impl<T: Display> Display for ErrorVec<T> {
impl<T> FromStr for ErrorVec<T> { impl<T> FromStr for ErrorVec<T> {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(_: &str) -> Result<Self, Self::Err> {
Ok(ErrorVec { errors: Vec::new() }) Ok(ErrorVec { errors: Vec::new() })
} }
} }

View File

@ -1,3 +1,4 @@
pub(crate) mod error; pub(crate) mod error;
pub(crate) mod error_vec; pub(crate) mod error_vec;
pub(crate) mod project_create_error; pub(crate) mod project_create_error;
pub(crate) mod task_create_error;

View File

@ -12,8 +12,8 @@ pub enum ProjectCreateError {
} }
impl From<ValidationErrors> for ErrorVec<ProjectCreateError> { impl From<ValidationErrors> for ErrorVec<ProjectCreateError> {
fn from(e: ValidationErrors) -> Self { fn from(validation_errors: ValidationErrors) -> Self {
e.errors() validation_errors.errors()
.iter() .iter()
.flat_map(|(&field, error_kind)| match field { .flat_map(|(&field, error_kind)| match field {
"title" => match error_kind { "title" => match error_kind {
@ -22,30 +22,30 @@ impl From<ValidationErrors> for ErrorVec<ProjectCreateError> {
.map(|validation_error| validation_error.code.as_ref()) .map(|validation_error| validation_error.code.as_ref())
.map(|code| match code { .map(|code| match code {
"title_length" => ProjectCreateError::TitleLengthInvalid, "title_length" => ProjectCreateError::TitleLengthInvalid,
_ => panic!("unexpected validation error code: {code}"), _ => panic!("Unexpected validation error code: `{code}`."),
}) })
.collect::<Vec<ProjectCreateError>>(), .collect::<Vec<ProjectCreateError>>(),
_ => panic!("unexpected validation error kind"), _ => panic!("Unexpected validation error kind."),
}, },
_ => panic!("unexpected validation field name: {field}"), _ => panic!("Unexpected validation field name: `{field}`."),
}) })
.collect::<Vec<ProjectCreateError>>() .collect::<Vec<ProjectCreateError>>()
.into() .into()
} }
} }
// has to be implemented for Dioxus server functions // Has to be implemented for Dioxus server functions.
impl Display for ProjectCreateError { impl Display for ProjectCreateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self) write!(f, "{:?}", self)
} }
} }
// has to be implemented for Dioxus server functions // Has to be implemented for Dioxus server functions.
impl FromStr for ProjectCreateError { impl FromStr for ProjectCreateError {
type Err = (); type Err = ();
fn from_str(_: &str) -> Result<Self, Self::Err> { fn from_str(_: &str) -> Result<Self, Self::Err> {
Ok(ProjectCreateError::TitleLengthInvalid) Ok(ProjectCreateError::Error(Error::ServerInternal))
} }
} }

View File

@ -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<ValidationErrors> for ErrorVec<TaskCreateError> {
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::<Vec<TaskCreateError>>(),
_ => panic!("Unexpected validation error kind."),
},
_ => panic!("Unexpected validation field name: `{field}`."),
})
.collect::<Vec<TaskCreateError>>()
.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<Self, Self::Err> {
Ok(TaskCreateError::Error(Error::ServerInternal))
}
}

65
src/models/category.rs Normal file
View File

@ -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<DurationSeconds<i64>>")]
reoccurance_interval: Option<Duration>,
time: Option<CalendarTime>,
},
LongTerm,
Done,
Trash,
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CalendarTime {
time: NaiveTime,
#[serde_as(as = "Option<DurationSeconds<i64>>")]
reminder_offset: Option<Duration>,
}
impl CalendarTime {
pub fn new(time: NaiveTime, reminder_offset: Option<Duration>) -> Self {
Self { time, reminder_offset }
}
}
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)?;
// Prepend the JSONB version byte.
out.write_all(&[1])?;
out.write_all(json.as_bytes())?;
Ok(diesel::serialize::IsNull::No)
}
}
impl FromSql<Jsonb, Pg> for Category {
fn from_sql(bytes: PgValue) -> diesel::deserialize::Result<Self> {
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)
}
}

View File

@ -1 +1,3 @@
pub(crate) mod project; pub(crate) mod project;
pub(crate) mod category;
pub(crate) mod task;

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, Debug)] #[derive(Queryable, Selectable, Serialize, Deserialize, 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 {

60
src/models/task.rs Normal file
View File

@ -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<chrono::NaiveDate>,
category: Category,
project_id: Option<i32>,
}
impl Task {
pub fn id(&self) -> i32 {
self.id
}
pub fn title(&self) -> &str {
&self.title
}
pub fn deadline(&self) -> Option<chrono::NaiveDate> {
self.deadline
}
pub fn category(&self) -> &Category {
&self.category
}
pub fn project_id(&self) -> Option<i32> {
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<chrono::NaiveDate>,
pub category: Category,
pub project_id: Option<i32>,
}
impl NewTask {
pub fn new(
title: String, deadline: Option<chrono::NaiveDate>,
category: Category, project_id: Option<i32>,
) -> Self {
Self { title, deadline, category, project_id }
}
}

View File

@ -6,3 +6,20 @@ diesel::table! {
title -> Text, title -> Text,
} }
} }
diesel::table! {
tasks (id) {
id -> Int4,
title -> Text,
deadline -> Nullable<Date>,
category -> Jsonb,
project_id -> Nullable<Int4>,
}
}
diesel::joinable!(tasks -> projects (project_id));
diesel::allow_tables_to_appear_in_same_query!(
projects,
tasks,
);

View File

@ -1,2 +1,3 @@
mod database_connection; mod database_connection;
pub(crate) mod projects; pub(crate) mod projects;
pub(crate) mod tasks;

View File

@ -3,23 +3,21 @@ use crate::errors::error_vec::ErrorVec;
use crate::errors::project_create_error::ProjectCreateError; use crate::errors::project_create_error::ProjectCreateError;
use crate::models::project::{NewProject, Project}; use crate::models::project::{NewProject, Project};
use crate::server::database_connection::establish_database_connection; use crate::server::database_connection::establish_database_connection;
use diesel::{RunQueryDsl, SelectableHelper}; use diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
use dioxus::prelude::*; use dioxus::prelude::*;
use validator::Validate; use validator::Validate;
#[server] #[server]
pub(crate) async fn create_project( pub(crate) async fn create_project(new_project: NewProject)
new_project: NewProject, -> Result<Project, ServerFnError<ErrorVec<ProjectCreateError>>> {
) -> Result<Project, ServerFnError<ErrorVec<ProjectCreateError>>> {
use crate::schema::projects; use crate::schema::projects;
new_project new_project.validate()
.validate()
.map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?; .map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?;
let mut connection = establish_database_connection() let mut connection = establish_database_connection()
.map_err::<ErrorVec<ProjectCreateError>, _>( .map_err::<ErrorVec<ProjectCreateError>, _>(
|_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into() |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into()
)?; )?;
let new_project = diesel::insert_into(projects::table) let new_project = diesel::insert_into(projects::table)
@ -27,8 +25,28 @@ pub(crate) async fn create_project(
.returning(Project::as_returning()) .returning(Project::as_returning())
.get_result(&mut connection) .get_result(&mut connection)
.map_err::<ErrorVec<ProjectCreateError>, _>( .map_err::<ErrorVec<ProjectCreateError>, _>(
|_| vec![ProjectCreateError::Error(Error::ServerInternal), ].into() |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into()
)?; )?;
Ok(new_project) Ok(new_project)
} }
#[server]
pub(crate) async fn get_projects()
-> Result<Vec<Project>, ServerFnError<ErrorVec<Error>>> {
use crate::schema::projects::dsl::*;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<Error>, _>(
|_| vec![Error::ServerInternal].into()
)?;
let results = projects
.select(Project::as_select())
.load::<Project>(&mut connection)
.map_err::<ErrorVec<Error>, _>(
|_| vec![Error::ServerInternal].into()
)?;
Ok(results)
}

45
src/server/tasks.rs Normal file
View File

@ -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<Task, ServerFnError<ErrorVec<TaskCreateError>>> {
use crate::schema::tasks;
new_task.validate()
.map_err::<ErrorVec<TaskCreateError>, _>(|errors| errors.into())?;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<TaskCreateError>, _>(
|_| 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::<ErrorVec<TaskCreateError>, _>(|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)
}

View File

@ -1,8 +1,15 @@
/* stylelint-disable */ /* stylelint-disable */
/* noinspection CssInvalidAtRule */ /* noinspection CssInvalidAtRule */
@tailwind base; @tailwind base;
/* noinspection CssInvalidAtRule */ /* noinspection CssInvalidAtRule */
@tailwind components; @tailwind components;
/* noinspection CssInvalidAtRule */ /* noinspection CssInvalidAtRule */
@tailwind utilities; @tailwind utilities;
html, body, #main {
/* noinspection CssInvalidAtRule */
@apply min-h-screen;
}
/* stylelint-enable */ /* stylelint-enable */