feat: ability to view tasks in a category (#19)

This commit is contained in:
Matouš Volf 2024-09-06 08:07:35 +02:00 committed by GitHub
commit b27f7d08c4
45 changed files with 1936 additions and 230 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="RR-242.20224.309">
<component name="dataSourceStorageLocal" created-in="RR-242.21829.114">
<data-source name="todo_baggins@localhost" uuid="1658668c-c2b8-426d-a22f-16fbad9eff0b">
<database-info product="PostgreSQL" version="16.4 (Debian 16.4-1.pgdg120+1)" jdbc-version="4.2" driver-name="PostgreSQL JDBC Driver" driver-version="42.6.0" dbms="POSTGRES" exact-version="16.4" exact-driver-version="42.6">
<identifier-quote-string>&quot;</identifier-quote-string>

14
.idea/webResources.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/assets" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

451
Cargo.lock generated
View File

@ -77,6 +77,17 @@ version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.3.1"
@ -89,6 +100,119 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand 2.1.0",
"futures-lite 2.3.0",
"slab",
]
[[package]]
name = "async-global-executor"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
"async-channel 2.3.1",
"async-executor",
"async-io 2.3.4",
"async-lock 3.4.0",
"blocking",
"futures-lite 2.3.0",
"once_cell",
]
[[package]]
name = "async-io"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
dependencies = [
"async-lock 2.8.0",
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-lite 1.13.0",
"log",
"parking",
"polling 2.8.0",
"rustix 0.37.27",
"slab",
"socket2 0.4.10",
"waker-fn",
]
[[package]]
name = "async-io"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
dependencies = [
"async-lock 3.4.0",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite 2.3.0",
"parking",
"polling 3.7.3",
"rustix 0.38.34",
"slab",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "async-lock"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [
"event-listener 2.5.3",
]
[[package]]
name = "async-lock"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
dependencies = [
"event-listener 5.3.1",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-std"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [
"async-channel 1.9.0",
"async-global-executor",
"async-io 1.13.0",
"async-lock 2.8.0",
"crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
"futures-lite 1.13.0",
"gloo-timers",
"kv-log-macro",
"log",
"memchr",
"once_cell",
"pin-project-lite",
"pin-utils",
"slab",
"wasm-bindgen-futures",
]
[[package]]
name = "async-task"
version = "4.7.1"
@ -226,6 +350,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
@ -250,10 +380,10 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
dependencies = [
"async-channel",
"async-channel 2.3.1",
"async-task",
"futures-io",
"futures-lite",
"futures-lite 2.3.0",
"piper",
]
@ -341,9 +471,10 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"pure-rust-locales",
"serde",
"wasm-bindgen",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -534,7 +665,7 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf97ee7261bb708fa3402fa9c17a54b70e90e3cb98afb3dc8999d5512cb03f94"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"byteorder",
"chrono",
"diesel_derives",
@ -830,6 +961,17 @@ dependencies = [
"tracing-wasm",
]
[[package]]
name = "dioxus-query"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c84184c06ee2823957aa3ef939da6a04f0ab934f007745eb65dadb105bde241"
dependencies = [
"dioxus-lib",
"futures-util",
"instant",
]
[[package]]
name = "dioxus-router"
version = "0.5.6"
@ -1016,6 +1158,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "euclid"
version = "0.22.10"
@ -1026,6 +1178,12 @@ dependencies = [
"serde",
]
[[package]]
name = "event-listener"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "5.3.1"
@ -1043,10 +1201,19 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener",
"event-listener 5.3.1",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.1.0"
@ -1122,13 +1289,31 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-lite"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand 2.1.0",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
@ -1362,6 +1547,8 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
@ -1453,6 +1640,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hex"
version = "0.4.3"
@ -1617,6 +1810,18 @@ dependencies = [
"serde",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "internment"
version = "0.7.5"
@ -1659,6 +1864,17 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "itoa"
version = "1.0.11"
@ -1680,7 +1896,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"serde",
"unicode-segmentation",
]
@ -1698,6 +1914,15 @@ dependencies = [
"semver",
]
[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
dependencies = [
"log",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -1710,6 +1935,18 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -1725,6 +1962,9 @@ name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
dependencies = [
"value-bag",
]
[[package]]
name = "longest-increasing-subsequence"
@ -1790,10 +2030,10 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
"wasi",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
@ -1894,7 +2134,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@ -1952,10 +2192,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
dependencies = [
"atomic-waker",
"fastrand",
"fastrand 2.1.0",
"futures-io",
]
[[package]]
name = "polling"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"concurrent-queue",
"libc",
"log",
"pin-project-lite",
"windows-sys 0.48.0",
]
[[package]]
name = "polling"
version = "3.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix 0.38.34",
"tracing",
"windows-sys 0.59.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2023,6 +2294,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pure-rust-locales"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1190fd18ae6ce9e137184f207593877e70f39b015040156b1e05081cdfe3733a"
[[package]]
name = "quote"
version = "1.0.36"
@ -2068,7 +2345,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
dependencies = [
"bitflags",
"bitflags 2.6.0",
]
[[package]]
@ -2121,6 +2398,33 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.3.8",
"windows-sys 0.48.0",
]
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys 0.4.14",
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.17"
@ -2433,6 +2737,16 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "socket2"
version = "0.5.7"
@ -2440,7 +2754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
@ -2583,10 +2897,12 @@ checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
name = "todo-baggins"
version = "0.1.0"
dependencies = [
"async-std",
"chrono",
"diesel",
"dioxus",
"dioxus-logger",
"dioxus-query",
"dotenvy",
"serde",
"serde_json",
@ -2609,9 +2925,9 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.5.7",
"tokio-macros",
"windows-sys",
"windows-sys 0.52.0",
]
[[package]]
@ -2686,7 +3002,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
"bitflags",
"bitflags 2.6.0",
"bytes",
"futures-util",
"http 1.1.0",
@ -2922,6 +3238,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -2934,6 +3256,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "waker-fn"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -3058,7 +3386,16 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
@ -3067,7 +3404,31 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
@ -3076,28 +3437,46 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3110,24 +3489,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@ -7,7 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
chrono = { version = "0.4.38", features = ["serde", "unstable-locales"] }
diesel = { version = "2.2.2", features = ["chrono", "postgres", "postgres_backend", "serde_json"] }
dioxus = { version = "0.5", features = ["fullstack", "router"] }
@ -21,6 +21,8 @@ serde_json = "1.0.125"
tracing = "0.1.40"
tracing-wasm = "0.2.1"
serde_with = { version = "3.9.0", features = ["chrono_0_4"] }
async-std = "1.12.0"
dioxus-query = "0.5.1"
[features]
default = []

View File

@ -31,10 +31,15 @@ watch_path = ["src", "assets"]
# CSS style file
style = ["/styles/tailwind_output.css"]
style = [
"/styles/tailwind_output.css",
"/styles/fonts.css",
"/styles/input_number_arrows.css",
"/styles/input_range.css"
]
# Javascript code file
script = []
script = ["https://kit.fontawesome.com/3c1b409f8f.js"]
[web.resource.dev]

17
assets/styles/fonts.css Normal file
View File

@ -0,0 +1,17 @@
@layer base {
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/fonts/inter_variable.woff2") format("woff2");
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url("/fonts/inter_variable_italic.woff2") format("woff2");
}
}

View File

@ -0,0 +1,10 @@
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
display: none;
appearance: none;
margin: 0;
}
input[type="number"] {
appearance:textfield;
}

View File

@ -0,0 +1,63 @@
input[type="range"],
input[type="range"]::-webkit-slider-runnable-track,
input[type="range"]::-webkit-slider-thumb {
appearance: none;
}
input[type="range"] {
background: transparent;
}
input[type="range"]::-moz-range-thumb {
width: 1.25rem;
height: 1.25rem;
background: rgba(228 228 231);
border: 0;
border-radius: 0.5rem;
}
input[type="range"]::-moz-range-progress {
background: #525259;
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"]::-moz-range-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-moz-range-progress {
background: #2d2d31;
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-moz-range-track {
background: rgba(113 113 122 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"]::-webkit-slider-thumb {
width: 1.25rem;
height: 1.25rem;
background: rgba(228 228 231);
border: 0;
border-radius: 0.5rem;
position: relative;
top: -0.4rem;
}
input[type="range"]::-webkit-slider-runnable-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}
input[type="range"].input-range-reverse::-webkit-slider-runnable-track {
background: rgba(39 39 42 / 50%);
height: 0.5rem;
border-radius: 0.25rem;
}

View File

@ -2,12 +2,16 @@ use crate::route::Route;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_query::prelude::{use_init_query_client};
use crate::query::{QueryErrors, QueryKey, QueryValue};
#[component]
pub(crate) fn App() -> Element {
use_init_query_client::<QueryValue, QueryErrors, QueryKey>();
rsx! {
div {
class: "min-h-screen text-white bg-neutral-800",
class: "min-h-screen text-zinc-200 bg-zinc-800 pt-4 pb-36",
Router::<Route> {}
}
}

View File

@ -0,0 +1,62 @@
use dioxus::prelude::*;
use crate::components::navigation::Navigation;
use crate::components::project_form::ProjectForm;
use crate::components::task_form::TaskForm;
use crate::route::Route;
#[component]
pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
// A signal for delaying the application of styles.
#[allow(clippy::redundant_closure)]
let mut expanded = use_signal(|| display_form());
let navigation_expanded = use_signal(|| false);
let current_route = use_route();
use_effect(use_reactive(&display_form, move |creating_task| {
if creating_task() {
expanded.set(true);
} else {
spawn(async move {
// Necessary for a smooth not instant height transition.
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
expanded.set(false);
});
}
}));
rsx! {
div {
class: format!(
"bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] {}",
match (display_form(), current_route, navigation_expanded()) {
(false, _, false) => "h-[64px]",
(false, _, true) => "h-[128px]",
(true, Route::ProjectsPage, _) => "h-[128px]",
(true, _, _) => "h-[448px]",
}
),
if expanded() {
match current_route {
Route::ProjectsPage => rsx! {
ProjectForm {
on_successful_submit: move |_| {
display_form.set(false);
}
}
},
_ => rsx! {
TaskForm {
on_successful_submit: move |_| {
display_form.set(false);
}
}
}
}
} else {
Navigation {
expanded: navigation_expanded,
}
}
}
}
}

View File

@ -0,0 +1,102 @@
use crate::models::category::Category;
use chrono::NaiveDate;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn CategoryInput(selected_category: Signal<Category>, class: Option<&'static str>) -> Element {
rsx! {
div {
class: format!("flex flex-row gap-2 {}", class.unwrap_or("")),
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if selected_category() == Category::SomedayMaybe { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::SomedayMaybe);
},
i {
class: "fa-solid fa-question"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if selected_category() == Category::LongTerm { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::LongTerm);
},
i {
class: "fa-solid fa-water"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if let Category::WaitingFor(_) = selected_category() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::WaitingFor(String::new()));
},
i {
class: "fa-solid fa-hourglass-half"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if selected_category() == Category::NextSteps { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::NextSteps);
},
i {
class: "fa-solid fa-forward"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if let Category::Calendar { .. } = selected_category() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::Calendar {
date: NaiveDate::default(),
reoccurrence: None,
time: None,
});
},
i {
class: "fa-solid fa-calendar-days"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg grow basis-0 {}",
if selected_category() == Category::Inbox { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
selected_category.set(Category::Inbox);
},
i {
class: "fa-solid fa-inbox"
}
}
}
}
}

View File

@ -0,0 +1,16 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn CreateButton(creating: Signal<bool>) -> Element {
rsx! {
button {
class: "m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200",
onclick: move |_| {
creating.set(!creating());
},
i {
class: format!("min-w-6 fa-solid {}", if creating() { "fa-xmark" } else { "fa-plus" }),
}
}
}
}

View File

@ -1,13 +1,9 @@
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 {}
}
}

24
src/components/layout.rs Normal file
View File

@ -0,0 +1,24 @@
use crate::components::bottom_panel::BottomPanel;
use crate::route::Route;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::create_task_button::CreateButton;
use crate::components::sticky_bottom::StickyBottom;
#[component]
pub(crate) fn Layout() -> Element {
let display_form = use_signal(|| false);
rsx! {
Outlet::<Route> {}
StickyBottom {
CreateButton {
creating: display_form,
}
BottomPanel {
display_form: display_form,
}
}
}
}

View File

@ -2,3 +2,13 @@ pub(crate) mod app;
pub(crate) mod home;
pub(crate) mod project_form;
pub(crate) mod task_form;
pub(crate) mod task_list;
pub(crate) mod pages;
pub(crate) mod navigation;
pub(crate) mod create_task_button;
pub(crate) mod bottom_panel;
pub(crate) mod sticky_bottom;
pub(crate) mod category_input;
pub(crate) mod reoccurrence_input;
pub(crate) mod layout;
pub(crate) mod navigation_item;

View File

@ -0,0 +1,81 @@
use crate::components::navigation_item::NavigationItem;
use crate::route::Route;
use dioxus::prelude::*;
#[component]
pub(crate) fn Navigation(expanded: Signal<bool>) -> Element {
rsx! {
div {
class: "grid grid-cols-5 justify-stretch",
button {
class: format!(
"py-4 text-center text-2xl {}",
if expanded() { "text-zinc-200" }
else { "text-zinc-500" }
),
onclick: move |_| expanded.set(!expanded()),
i {
class: "fa-solid fa-bars"
}
},
NavigationItem {
route: Route::CategoryNextStepsPage,
i {
class: "fa-solid fa-forward"
}
},
NavigationItem {
route: Route::CategoryCalendarPage,
i {
class: "fa-solid fa-calendar-days"
}
},
NavigationItem {
route: Route::CategoryTodayPage,
i {
class: "fa-solid fa-calendar-day"
}
},
NavigationItem {
route: Route::CategoryInboxPage,
i {
class: "fa-solid fa-inbox"
}
},
{if expanded() {
rsx! {
NavigationItem {
route: Route::ProjectsPage,
i {
class: "fa-solid fa-list"
}
},
NavigationItem {
route: Route::CategoryTrashPage,
i {
class: "fa-solid fa-trash-can"
}
},
NavigationItem {
route: Route::CategoryDonePage,
i {
class: "fa-solid fa-check"
}
},
NavigationItem {
route: Route::CategoryLongTermPage,
i {
class: "fa-solid fa-water"
}
},
NavigationItem {
route: Route::CategoryWaitingForPage,
i {
class: "fa-solid fa-hourglass-half"
}
}
}
} else { None }}
}
}
}

View File

@ -0,0 +1,19 @@
use dioxus::prelude::*;
use crate::route::Route;
#[component]
pub(crate) fn NavigationItem(route: Route, children: Element) -> Element {
let current_route = use_route::<Route>();
rsx! {
Link {
to: route.clone(),
class: format!(
"py-4 text-center text-2xl {}",
if current_route == route { "text-zinc-200" }
else { "text-zinc-500" }
),
children
}
}
}

View File

@ -0,0 +1,78 @@
use crate::models::category::Category;
use chrono::{Datelike, Local, Locale};
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
use crate::components::task_list::TaskList;
use crate::query::QueryValue;
use crate::query::tasks::use_tasks_in_category_query;
use crate::models::task::Task;
const CALENDAR_LENGTH_DAYS: usize = 366 * 3;
#[component]
pub(crate) fn CategoryCalendarPage() -> Element {
let tasks = use_tasks_in_category_query(Category::Calendar {
date: Local::now().date_naive(),
reoccurrence: None,
time: None,
});
let tasks_query_result = tasks.result();
rsx! {
match tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => {
let today_date = Local::now().date_naive();
rsx! {
div {
class: "pt-4 flex flex-col gap-8",
for date_current in today_date.iter_days().take(CALENDAR_LENGTH_DAYS) {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
div {
class: "pt-1",
{
date_current
.format_localized(
format!(
"%A %-d. %B{}",
if date_current.year() != today_date.year() {" %Y"}
else {""}
).as_str(),
Locale::en_US
)
.to_string()
}
}
}
TaskList {
tasks: tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
*date == date_current
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>()
}
}
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryDonePage() -> Element {
rsx! {
CategoryPage {
category: Category::Done,
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryInboxPage() -> Element {
rsx! {
CategoryPage {
category: Category::Inbox,
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryLongTermPage() -> Element {
rsx! {
CategoryPage {
category: Category::LongTerm,
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryNextStepsPage() -> Element {
rsx! {
CategoryPage {
category: Category::NextSteps,
}
}
}

View File

@ -0,0 +1,33 @@
use crate::components::task_list::TaskList;
use crate::models::category::Category;
use crate::query::tasks::use_tasks_in_category_query;
use crate::query::QueryValue;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
#[component]
pub(crate) fn CategoryPage(category: Category) -> Element {
let tasks_query = use_tasks_in_category_query(category);
let tasks_query_result = tasks_query.result();
match tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! {
TaskList {
tasks: tasks.clone(),
class: "pb-36"
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategorySomedayMaybePage() -> Element {
rsx! {
CategoryPage {
category: Category::SomedayMaybe,
}
}
}

View File

@ -0,0 +1,160 @@
use crate::components::task_list::TaskList;
use crate::models::category::Category;
use crate::models::task::Task;
use crate::query::tasks::use_tasks_in_category_query;
use crate::query::QueryValue;
use chrono::{Local, Locale};
use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
#[component]
pub(crate) fn CategoryTodayPage() -> Element {
let today_date = Local::now().date_naive();
let calendar_tasks_query = use_tasks_in_category_query(Category::Calendar {
date: today_date,
reoccurrence: None,
time: None,
});
let calendar_tasks_query_result = calendar_tasks_query.result();
let long_term_tasks_query = use_tasks_in_category_query(Category::LongTerm);
let long_term_tasks_query_result = long_term_tasks_query.result();
rsx! {
div {
class: "pt-4 flex flex-col gap-8",
match long_term_tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-water text-xl w-6 text-center"
}
div {
class: "mt-1",
"Long-term"
}
}
div {
for task in tasks {
div {
key: "{task.id()}",
class: format!(
"px-8 pt-5 {} flex flex-row gap-4",
if task.deadline().is_some() {
"pb-0.5"
} else {
"pb-5"
}
),
div {
class: "flex flex-col",
div {
class: "mt grow font-medium",
{task.title()}
},
div {
class: "flex flex-row gap-3",
if let Some(deadline) = task.deadline() {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-bomb"
},
{deadline.format(" %m. %d.").to_string()}
}
}
}
}
}
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
match calendar_tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => {
let today_tasks = tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
*date == today_date
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>();
let overdue_tasks = tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
*date < today_date
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>();
rsx! {
if !overdue_tasks.is_empty() {
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-calendar-xmark text-xl w-6 text-center"
}
div {
class: "mt-1",
"Overdue"
}
}
TaskList {
tasks: overdue_tasks,
class: "pb-3"
}
}
}
div {
class: "flex flex-col gap-4",
div {
class: "px-8 flex flex-row items-center gap-2 font-bold",
i {
class: "fa-solid fa-calendar-check text-xl w-6 text-center"
}
div {
class: "mt-1",
{
today_date
.format_localized("Today, %A %-d. %B", Locale::en_US)
.to_string()
}
}
}
TaskList {
tasks: today_tasks
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryTrashPage() -> Element {
rsx! {
CategoryPage {
category: Category::Trash,
}
}
}

View File

@ -0,0 +1,14 @@
use crate::models::category::Category;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use crate::components::pages::category_page::CategoryPage;
#[component]
pub(crate) fn CategoryWaitingForPage() -> Element {
rsx! {
CategoryPage {
category: Category::WaitingFor(String::new()),
}
}
}

View File

@ -0,0 +1,12 @@
pub(crate) mod category_inbox_page;
pub(crate) mod category_calendar_page;
pub(crate) mod category_today_page;
pub(crate) mod category_waiting_for_page;
pub(crate) mod category_long_term_page;
pub(crate) mod category_next_steps_page;
pub(crate) mod category_someday_maybe_page;
pub(crate) mod category_done_page;
pub(crate) mod category_trash_page;
pub(crate) mod not_found_page;
pub(crate) mod projects_page;
pub(crate) mod category_page;

View File

@ -0,0 +1,8 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn NotFoundPage(route: Vec<String>) -> Element {
rsx! {
{"404"}
}
}

View File

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

View File

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

View File

@ -0,0 +1,76 @@
use crate::models::category::ReoccurrenceInterval;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn ReoccurrenceIntervalInput(
reoccurrence_interval: Signal<Option<ReoccurrenceInterval>>,
class_buttons: Option<&'static str>
) -> Element {
rsx! {
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {}",
class_buttons.unwrap_or(""),
if reoccurrence_interval().is_none() { "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(None);
},
i {
class: "fa-solid fa-ban"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {}",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Day) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Day))
},
i {
class: "fa-solid fa-sun"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {}",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Month) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Month))
},
i {
class: "fa-solid fa-moon"
}
},
button {
r#type: "button",
class: format!(
"py-2 rounded-lg {} {}",
class_buttons.unwrap_or(""),
if let Some(ReoccurrenceInterval::Year) = reoccurrence_interval()
{ "bg-zinc-500/50" }
else { "bg-zinc-800/50" }
),
onclick: move |_| {
reoccurrence_interval.set(Some(ReoccurrenceInterval::Year))
},
i {
class: "fa-solid fa-earth-europe"
}
}
}
}

View File

@ -0,0 +1,11 @@
use dioxus::prelude::*;
#[component]
pub(crate) fn StickyBottom(children: Element) -> Element {
rsx! {
div {
class: "fixed bottom-0 left-0 right-0 flex flex-col",
{children}
}
}
}

View File

@ -1,45 +1,68 @@
use chrono::Duration;
use crate::models::category::{CalendarTime, Category};
use crate::components::category_input::CategoryInput;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask;
use crate::server::projects::get_projects;
use crate::server::tasks::create_task;
use chrono::{Duration, NaiveDate};
use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::route::Route;
const REMINDER_OFFSETS: [Option<Duration>; 17] = [
None,
Some(Duration::days(1)),
Some(Duration::hours(12)),
Some(Duration::hours(11)),
Some(Duration::hours(10)),
Some(Duration::hours(9)),
Some(Duration::hours(8)),
Some(Duration::hours(7)),
Some(Duration::hours(6)),
Some(Duration::hours(5)),
Some(Duration::hours(4)),
Some(Duration::hours(3)),
Some(Duration::hours(2)),
Some(Duration::hours(1)),
Some(Duration::minutes(30)),
Some(Duration::minutes(10)),
Some(Duration::zero()),
];
#[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,
];
pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
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);
let route = use_route::<Route>();
let selected_category = use_signal(|| match route {
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
Route::CategoryNextStepsPage => Category::NextSteps,
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
date: NaiveDate::default(),
reoccurrence: None,
time: None,
},
Route::CategoryLongTermPage => Category::LongTerm,
_ => Category::Inbox,
});
let category_calendar_reoccurrence_interval = use_signal(|| None);
let mut category_calendar_has_time = use_signal(|| false);
let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1);
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
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()
] {
match &selected_category() {
Category::WaitingFor(_) => Category::WaitingFor(
event.values().get("category_waiting_for").unwrap()
.as_value()
@ -47,24 +70,24 @@ pub(crate) fn TaskForm() -> Element {
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")
reoccurrence: category_calendar_reoccurrence_interval().map(
|reoccurrence_interval| Reoccurrence::new(
event.values().get("category_calendar_date").unwrap()
.as_value().parse().unwrap(),
reoccurrence_interval,
event.values().get("category_calendar_reoccurrence_length")
.unwrap().as_value().parse().unwrap()
)
),
time: event.values().get("category_calendar_time").unwrap()
.as_value().parse().ok().map(|time|
.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()
)
)
REMINDER_OFFSETS[
event.values()
.get("category_calendar_reminder_offset_index").unwrap()
.as_value().parse::<usize>().unwrap()
]
)
)
},
@ -74,148 +97,215 @@ pub(crate) fn TaskForm() -> Element {
.as_value().parse::<i32>().ok().filter(|&id| id > 0),
);
let _ = create_task(new_task).await;
query_client.invalidate_queries(&[
QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category())
]);
on_successful_submit.call(());
}
},
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",
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-pen-clip text-zinc-400/50"
},
},
Category::Calendar { .. } => rsx !{
input {
r#type: "date",
name: "category_calendar_date",
required: true,
class: "p-2 bg-neutral-700 rounded",
input {
r#type: "text",
name: "title",
required: true,
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
},
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_project",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list text-zinc-400/50"
}
},
select {
name: "project_id",
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow",
id: "input_project",
option {
value: 0,
"None"
},
for project in projects {
option {
value: project.id().to_string(),
{project.title()}
}
}
},
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bomb text-zinc-400/50"
}
},
input {
r#type: "date",
name: "deadline",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0",
id: "input_deadline"
}
},
div {
class: "flex flex-row items-center gap-3",
label {
class: "min-w-6 text-center",
i {
class: "fa-solid fa-layer-group text-zinc-400/50"
}
},
CategoryInput {
selected_category: selected_category,
class: "grow"
}
}
match selected_category() {
Category::WaitingFor(_) => rsx! {
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());
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-hourglass-end text-zinc-400/50"
}
},
label {
r#for: "category_calendar_is_reoccurring",
" is reoccurring"
}
},
if category_calendar_is_reoccurring() {
input {
r#type: "number",
name: "category_calendar_reoccurance_interval",
r#type: "text",
name: "category_waiting_for",
required: true,
min: 1,
placeholder: "reoccurance interval (days)",
class: "p-2 bg-neutral-700 rounded",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_waiting_for"
},
}
},
Category::Calendar { .. } => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_category_calendar_date",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-clock text-zinc-400/50"
}
},
div {
class: "grow flex flex-row gap-2",
input {
r#type: "date",
name: "category_calendar_date",
required: true,
initial_value: chrono::Local::now().format("%Y-%m-%d").to_string(),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_date"
},
input {
r#type: "time",
name: "category_calendar_time",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_time",
oninput: move |event| {
category_calendar_has_time.set(!event.value().is_empty());
}
}
}
},
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());
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reoccurrence_length",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-repeat text-zinc-400/50"
}
},
div {
class: "grow grid grid-cols-6 gap-2",
ReoccurrenceIntervalInput {
reoccurrence_interval: category_calendar_reoccurrence_interval
},
input {
r#type: "number",
inputmode: "numeric",
name: "category_calendar_reoccurrence_length",
disabled: category_calendar_reoccurrence_interval().is_none(),
required: true,
min: 1,
initial_value:
if category_calendar_reoccurrence_interval().is_none() { "" }
else { "1" },
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
id: "category_calendar_reoccurrence_length"
}
}
},
if category_calendar_has_time() {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reminder_offset_index",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bell text-zinc-400/50"
}
},
input {
r#type: "checkbox",
name: "category_calendar_has_reminder",
value: 0,
r#type: "range",
name: "category_calendar_reminder_offset_index",
min: 0,
max: REMINDER_OFFSETS.len() as i64 - 1,
initial_value: REMINDER_OFFSETS.len() as i64 - 1,
class: "grow input-range-reverse",
id: "category_calendar_has_reminder",
onchange: move |event| {
category_calendar_has_reminder.set(event.checked());
oninput: move |event| {
category_calendar_reminder_offset_index.set(
event.value().parse().unwrap()
);
}
},
label {
r#for: "category_calendar_has_reminder",
" set a reminder"
r#for: "category_calendar_reminder_offset_index",
class: "pr-3 min-w-16 text-right",
{REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map(
|offset| if offset.num_hours() < 1 {
format!("{} min", offset.num_minutes())
} else {
format!("{} h", offset.num_hours())
}
).unwrap_or_else(|| "none".to_string())}
}
}
}
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()}
div {
class: "flex flex-row justify-end mt-auto",
button {
r#type: "submit",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg",
i {
class: "fa-solid fa-floppy-disk"
}
}
},
button {
r#type: "submit",
"create"
}
}
}
}
}

View File

@ -0,0 +1,66 @@
use crate::models::category::Category;
use crate::models::task::Task;
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
#[component]
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
rsx! {
div {
class: format!("flex flex-col {}", class.unwrap_or("")),
for task in tasks {
div {
key: "{task.id()}",
class: format!(
"px-8 pt-5 {} flex flex-row gap-4",
if task.deadline().is_some() {
"pb-0.5"
} else if let Category::Calendar { time, .. } = task.category() {
if time.is_some() {
"pb-0.5"
} else {
"pb-5"
}
} else {
"pb-5"
}
),
i {
class: "fa-regular fa-square text-3xl text-zinc-600",
},
div {
class: "flex flex-col",
div {
class: "mt-1 grow font-medium",
{task.title()}
},
div {
class: "flex flex-row gap-3",
if let Some(deadline) = task.deadline() {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-bomb"
},
{deadline.format(" %m. %d.").to_string()}
}
}
if let Category::Calendar { time, .. } = task.category() {
if let Some(calendar_time) = time {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-clock"
},
{calendar_time.time().format(" %k:%M").to_string()}
}
}
}
}
}
}
}
}
}
}

View File

@ -4,6 +4,7 @@ mod models;
mod route;
mod schema;
mod server;
mod query;
use components::app::App;
use dioxus::prelude::*;

View File

@ -1,10 +1,13 @@
use std::hash::Hash;
use crate::schema::tasks;
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 diesel::sql_types::{Bool, Jsonb};
use diesel::{AsExpression, BoxableExpression, FromSqlRow, PgJsonbExpressionMethods};
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_with::DurationSeconds;
use std::io::Write;
@ -18,8 +21,7 @@ pub enum Category {
NextSteps,
Calendar {
date: NaiveDate,
#[serde_as(as = "Option<DurationSeconds<i64>>")]
reoccurance_interval: Option<Duration>,
reoccurrence: Option<Reoccurrence>,
time: Option<CalendarTime>,
},
LongTerm,
@ -27,20 +29,37 @@ pub enum Category {
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 Category {
pub fn eq_sql_predicate(&self) -> Box<dyn BoxableExpression<tasks::table, Pg, SqlType=Bool>> {
use crate::schema::tasks::dsl::*;
impl CalendarTime {
pub fn new(time: NaiveTime, reminder_offset: Option<Duration>) -> Self {
Self { time, reminder_offset }
match self {
Category::Inbox => Box::new(category.contains(json!("Inbox"))),
Category::SomedayMaybe => Box::new(category.contains(json!("SomedayMaybe"))),
Category::WaitingFor(_) => Box::new(category.has_key("WaitingFor")),
Category::NextSteps => Box::new(category.contains(json!("NextSteps"))),
Category::Calendar { .. } => Box::new(category.has_key("Calendar")),
Category::LongTerm => Box::new(category.contains(json!("LongTerm"))),
Category::Done => Box::new(category.contains(json!("Done"))),
Category::Trash => Box::new(category.contains(json!("Trash"))),
}
}
}
impl Hash for Category {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
}
}
impl PartialEq for Category {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
impl Eq for Category {}
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)?;
@ -63,3 +82,53 @@ impl FromSql<Jsonb, Pg> for Category {
serde_json::from_str(str).map_err(Into::into)
}
}
#[derive(Serialize, Deserialize, Hash, Clone, Debug)]
pub enum ReoccurrenceInterval {
Day,
Month,
Year,
}
#[derive(Serialize, Deserialize, Hash, Clone, Debug)]
pub struct Reoccurrence {
start_date: NaiveDate,
interval: ReoccurrenceInterval,
length: u32,
}
impl Reoccurrence {
pub fn new(start_date: NaiveDate, interval: ReoccurrenceInterval, length: u32) -> Self {
Self { start_date, interval, length }
}
pub fn interval(&self) -> &ReoccurrenceInterval {
&self.interval
}
pub fn length(&self) -> u32 {
self.length
}
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Hash, 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 }
}
pub fn time(&self) -> NaiveTime {
self.time
}
pub fn reminder_offset(&self) -> Option<Duration> {
self.reminder_offset
}
}

View File

@ -6,7 +6,7 @@ use validator::Validate;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)]
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[diesel(table_name = crate::schema::projects)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Project {

View File

@ -1,13 +1,13 @@
use crate::models::category::Category;
use crate::schema::tasks;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::models::category::Category;
use crate::schema::tasks;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, Clone, Debug)]
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[diesel(table_name = crate::schema::tasks)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Task {

26
src/query/mod.rs Normal file
View File

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

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

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

24
src/query/tasks.rs Normal file
View File

@ -0,0 +1,24 @@
use dioxus::prelude::ServerFnError;
use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery};
use crate::models::category::Category;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::tasks::get_tasks_in_category;
pub(crate) fn use_tasks_in_category_query(category: Category)
-> UseQuery<QueryValue, QueryErrors, QueryKey> {
use_get_query([QueryKey::TasksInCategory(category), QueryKey::Tasks], fetch_tasks_in_category)
}
async fn fetch_tasks_in_category(keys: Vec<QueryKey>) -> QueryResult<QueryValue, QueryErrors> {
if let Some(QueryKey::TasksInCategory(category)) = keys.first() {
match get_tasks_in_category(category.clone()).await {
Ok(tasks) => Ok(QueryValue::Tasks(tasks)),
Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)),
Err(error) => panic!("Unexpected error: {:?}", error)
}.into()
} else {
panic!("Unexpected query keys: {:?}", keys);
}
}

View File

@ -1,8 +1,48 @@
use crate::components::home::Home;
use crate::components::pages::category_inbox_page::CategoryInboxPage;
use crate::components::pages::category_next_steps_page::CategoryNextStepsPage;
use crate::components::pages::category_today_page::CategoryTodayPage;
use crate::components::pages::category_trash_page::CategoryTrashPage;
use crate::components::pages::category_waiting_for_page::CategoryWaitingForPage;
use crate::components::pages::category_someday_maybe_page::CategorySomedayMaybePage;
use crate::components::pages::category_done_page::CategoryDonePage;
use crate::components::pages::category_calendar_page::CategoryCalendarPage;
use crate::components::pages::category_long_term_page::CategoryLongTermPage;
use crate::components::pages::projects_page::ProjectsPage;
use crate::components::pages::not_found_page::NotFoundPage;
use crate::components::layout::Layout;
use dioxus::prelude::*;
// All variants have the same postfix because they have to match the component names.
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Routable, Debug, PartialEq)]
#[rustfmt::skip]
pub(crate) enum Route {
#[route("/")]
Home {},
#[layout(Layout)]
#[redirect("/", || Route::CategoryTodayPage {})]
#[route("/today")]
CategoryTodayPage,
#[route("/inbox")]
CategoryInboxPage,
#[route("/someday-maybe")]
CategorySomedayMaybePage,
#[route("/waiting-for")]
CategoryWaitingForPage,
#[route("/next-steps")]
CategoryNextStepsPage,
#[route("/calendar")]
CategoryCalendarPage,
#[route("/long-term")]
CategoryLongTermPage,
#[route("/done")]
CategoryDonePage,
#[route("/trash")]
CategoryTrashPage,
#[route("/projects")]
ProjectsPage,
#[end_layout]
#[redirect("/", || Route::CategoryTodayPage)]
#[route("/:..route")]
NotFoundPage {
route: Vec<String>,
},
}

View File

@ -2,10 +2,11 @@ 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 diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
use dioxus::prelude::*;
use validator::Validate;
use crate::errors::task_create_error::TaskCreateError;
use crate::models::category::Category;
#[server]
pub(crate) async fn create_task(new_task: NewTask)
@ -43,3 +44,24 @@ pub(crate) async fn create_task(new_task: NewTask)
Ok(new_task)
}
#[server]
pub(crate) async fn get_tasks_in_category(filtered_category: Category)
-> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> {
use crate::schema::tasks::dsl::*;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<Error>, _>(
|_| vec![Error::ServerInternal].into()
)?;
let results = tasks
.select(Task::as_select())
.filter(filtered_category.eq_sql_predicate())
.load::<Task>(&mut connection)
.map_err::<ErrorVec<Error>, _>(
|_| vec![Error::ServerInternal].into()
)?;
Ok(results)
}

View File

@ -13,21 +13,3 @@ html, body, #main {
}
/* stylelint-enable */
@layer base {
@font-face {
font-family: Inter;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("/fonts/inter_variable.woff2") format("woff2");
}
@font-face {
font-family: Inter;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url("/fonts/inter_variable_italic.woff2") format("woff2");
}
}