diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml index a0dc61f..5cb6af1 100644 --- a/.idea/dataSources.local.xml +++ b/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + " diff --git a/.idea/webResources.xml b/.idea/webResources.xml new file mode 100644 index 0000000..7356908 --- /dev/null +++ b/.idea/webResources.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7fcda7a..3e9311c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ac211f5..dee2047 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [] diff --git a/Dioxus.toml b/Dioxus.toml index e396b3a..44e6de0 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -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] diff --git a/assets/styles/fonts.css b/assets/styles/fonts.css new file mode 100644 index 0000000..e2cb5c0 --- /dev/null +++ b/assets/styles/fonts.css @@ -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"); + } +} diff --git a/assets/styles/input_number_arrows.css b/assets/styles/input_number_arrows.css new file mode 100644 index 0000000..4330152 --- /dev/null +++ b/assets/styles/input_number_arrows.css @@ -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; +} diff --git a/assets/styles/input_range.css b/assets/styles/input_range.css new file mode 100644 index 0000000..8cb8a42 --- /dev/null +++ b/assets/styles/input_range.css @@ -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; +} diff --git a/src/components/app.rs b/src/components/app.rs index 0ccd3a2..708b0f5 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -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::(); + 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:: {} } } diff --git a/src/components/bottom_panel.rs b/src/components/bottom_panel.rs new file mode 100644 index 0000000..c90d542 --- /dev/null +++ b/src/components/bottom_panel.rs @@ -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) -> 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, + } + } + } + } +} diff --git a/src/components/category_input.rs b/src/components/category_input.rs new file mode 100644 index 0000000..b9ffbab --- /dev/null +++ b/src/components/category_input.rs @@ -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, 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" + } + } + } + } +} diff --git a/src/components/create_task_button.rs b/src/components/create_task_button.rs new file mode 100644 index 0000000..25d69a2 --- /dev/null +++ b/src/components/create_task_button.rs @@ -0,0 +1,16 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn CreateButton(creating: Signal) -> 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" }), + } + } + } +} diff --git a/src/components/home.rs b/src/components/home.rs index 827fad4..4dd9aac 100644 --- a/src/components/home.rs +++ b/src/components/home.rs @@ -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 {} } } diff --git a/src/components/layout.rs b/src/components/layout.rs new file mode 100644 index 0000000..ed89b5f --- /dev/null +++ b/src/components/layout.rs @@ -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:: {} + StickyBottom { + CreateButton { + creating: display_form, + } + BottomPanel { + display_form: display_form, + } + } + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index c632a34..bd961d8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -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; diff --git a/src/components/navigation.rs b/src/components/navigation.rs new file mode 100644 index 0000000..854e635 --- /dev/null +++ b/src/components/navigation.rs @@ -0,0 +1,81 @@ +use crate::components::navigation_item::NavigationItem; +use crate::route::Route; +use dioxus::prelude::*; + +#[component] +pub(crate) fn Navigation(expanded: Signal) -> 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 }} + } + } +} diff --git a/src/components/navigation_item.rs b/src/components/navigation_item.rs new file mode 100644 index 0000000..5bfe82b --- /dev/null +++ b/src/components/navigation_item.rs @@ -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::(); + + 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 + } + } +} diff --git a/src/components/pages/category_calendar_page.rs b/src/components/pages/category_calendar_page.rs new file mode 100644 index 0000000..1a610b3 --- /dev/null +++ b/src/components/pages/category_calendar_page.rs @@ -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::>() + } + } + } + } + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + }, + value => panic!("Unexpected query result: {value:?}") + } + } +} diff --git a/src/components/pages/category_done_page.rs b/src/components/pages/category_done_page.rs new file mode 100644 index 0000000..ad65d31 --- /dev/null +++ b/src/components/pages/category_done_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_inbox_page.rs b/src/components/pages/category_inbox_page.rs new file mode 100644 index 0000000..debc408 --- /dev/null +++ b/src/components/pages/category_inbox_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_long_term_page.rs b/src/components/pages/category_long_term_page.rs new file mode 100644 index 0000000..437de37 --- /dev/null +++ b/src/components/pages/category_long_term_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_next_steps_page.rs b/src/components/pages/category_next_steps_page.rs new file mode 100644 index 0000000..c1086f3 --- /dev/null +++ b/src/components/pages/category_next_steps_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_page.rs b/src/components/pages/category_page.rs new file mode 100644 index 0000000..0dd2b0c --- /dev/null +++ b/src/components/pages/category_page.rs @@ -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:?}") + } +} diff --git a/src/components/pages/category_someday_maybe_page.rs b/src/components/pages/category_someday_maybe_page.rs new file mode 100644 index 0000000..7a2070f --- /dev/null +++ b/src/components/pages/category_someday_maybe_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_today_page.rs b/src/components/pages/category_today_page.rs new file mode 100644 index 0000000..47df487 --- /dev/null +++ b/src/components/pages/category_today_page.rs @@ -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::>(); + let overdue_tasks = tasks.iter().filter(|task| { + if let Category::Calendar { date, .. } = task.category() { + *date < today_date + } else { + panic!("Unexpected category."); + } + }).cloned().collect::>(); + + 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:?}") + } + } + } +} diff --git a/src/components/pages/category_trash_page.rs b/src/components/pages/category_trash_page.rs new file mode 100644 index 0000000..36b75f7 --- /dev/null +++ b/src/components/pages/category_trash_page.rs @@ -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, + } + } +} diff --git a/src/components/pages/category_waiting_for_page.rs b/src/components/pages/category_waiting_for_page.rs new file mode 100644 index 0000000..062373a --- /dev/null +++ b/src/components/pages/category_waiting_for_page.rs @@ -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()), + } + } +} diff --git a/src/components/pages/mod.rs b/src/components/pages/mod.rs new file mode 100644 index 0000000..9dcc2c3 --- /dev/null +++ b/src/components/pages/mod.rs @@ -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; diff --git a/src/components/pages/not_found_page.rs b/src/components/pages/not_found_page.rs new file mode 100644 index 0000000..599ec1c --- /dev/null +++ b/src/components/pages/not_found_page.rs @@ -0,0 +1,8 @@ +use dioxus::prelude::*; + +#[component] +pub(crate) fn NotFoundPage(route: Vec) -> Element { + rsx! { + {"404"} + } +} diff --git a/src/components/pages/projects_page.rs b/src/components/pages/projects_page.rs new file mode 100644 index 0000000..fd7f34e --- /dev/null +++ b/src/components/pages/projects_page.rs @@ -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:?}") + } + } +} diff --git a/src/components/project_form.rs b/src/components/project_form.rs index 6d64abf..fc7ad2f 100644 --- a/src/components/project_form.rs +++ b/src/components/project_form.rs @@ -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::(); + 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" + } + } } } } diff --git a/src/components/reoccurrence_input.rs b/src/components/reoccurrence_input.rs new file mode 100644 index 0000000..7f9360f --- /dev/null +++ b/src/components/reoccurrence_input.rs @@ -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>, + 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" + } + } + } +} diff --git a/src/components/sticky_bottom.rs b/src/components/sticky_bottom.rs new file mode 100644 index 0000000..972bc3f --- /dev/null +++ b/src/components/sticky_bottom.rs @@ -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} + } + } +} diff --git a/src/components/task_form.rs b/src/components/task_form.rs index a280437..95cabd1 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -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; 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::(|| 0); - let mut category_calendar_is_reoccurring = use_signal::(|| false); - let mut category_calendar_has_time = use_signal::(|| false); - let mut category_calendar_has_reminder = use_signal::(|| false); + let route = use_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::(); + rsx! { form { onsubmit: move |event| { - let categories = categories.clone(); async move { let new_task = NewTask::new( event.values().get("title").unwrap().as_value(), event.values().get("deadline").unwrap().as_value().parse().ok(), - match &categories[ - event.values().get("category_index").unwrap() - .as_value().parse::().unwrap() - ] { + 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::().unwrap() + ] ) ) }, @@ -74,148 +97,215 @@ pub(crate) fn TaskForm() -> Element { .as_value().parse::().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" } } -} + } } diff --git a/src/components/task_list.rs b/src/components/task_list.rs new file mode 100644 index 0000000..c9b04cb --- /dev/null +++ b/src/components/task_list.rs @@ -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, 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()} + } + } + } + } + } + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index b7109a5..a7a75b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod models; mod route; mod schema; mod server; +mod query; use components::app::App; use dioxus::prelude::*; diff --git a/src/models/category.rs b/src/models/category.rs index c6ca42c..f5a7838 100644 --- a/src/models/category.rs +++ b/src/models/category.rs @@ -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>")] - reoccurance_interval: Option, + reoccurrence: Option, time: Option, }, 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>")] - reminder_offset: Option, -} +impl Category { + pub fn eq_sql_predicate(&self) -> Box> { + use crate::schema::tasks::dsl::*; -impl CalendarTime { - pub fn new(time: NaiveTime, reminder_offset: Option) -> 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(&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 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 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>")] + reminder_offset: Option, +} + +impl CalendarTime { + pub fn new(time: NaiveTime, reminder_offset: Option) -> Self { + Self { time, reminder_offset } + } + + pub fn time(&self) -> NaiveTime { + self.time + } + + pub fn reminder_offset(&self) -> Option { + self.reminder_offset + } +} diff --git a/src/models/project.rs b/src/models/project.rs index 00aec2e..44898e6 100644 --- a/src/models/project.rs +++ b/src/models/project.rs @@ -6,7 +6,7 @@ use validator::Validate; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, 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 { diff --git a/src/models/task.rs b/src/models/task.rs index d67256a..4799ff2 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -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 { diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 0000000..7e1566a --- /dev/null +++ b/src/query/mod.rs @@ -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), + Projects(Vec), +} + +#[derive(Debug)] +pub(crate) enum QueryErrors { + Error(ErrorVec), +} + +#[derive(PartialEq, Eq, Hash, Clone, Debug)] +pub(crate) enum QueryKey { + Tasks, + TasksInCategory(Category), + Projects, +} diff --git a/src/query/projects.rs b/src/query/projects.rs new file mode 100644 index 0000000..174f330 --- /dev/null +++ b/src/query/projects.rs @@ -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 { + use_get_query([QueryKey::Projects, QueryKey::Tasks], fetch_projects) +} + +async fn fetch_projects(keys: Vec) -> QueryResult { + 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); + } +} diff --git a/src/query/tasks.rs b/src/query/tasks.rs new file mode 100644 index 0000000..1cf22fc --- /dev/null +++ b/src/query/tasks.rs @@ -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 { + use_get_query([QueryKey::TasksInCategory(category), QueryKey::Tasks], fetch_tasks_in_category) +} + +async fn fetch_tasks_in_category(keys: Vec) -> QueryResult { + 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); + } +} diff --git a/src/route/mod.rs b/src/route/mod.rs index 275f297..e0eb5c3 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -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, + }, } diff --git a/src/server/tasks.rs b/src/server/tasks.rs index 0b7c086..62c383a 100644 --- a/src/server/tasks.rs +++ b/src/server/tasks.rs @@ -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, ServerFnError>> { + use crate::schema::tasks::dsl::*; + + let mut connection = establish_database_connection() + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + let results = tasks + .select(Task::as_select()) + .filter(filtered_category.eq_sql_predicate()) + .load::(&mut connection) + .map_err::, _>( + |_| vec![Error::ServerInternal].into() + )?; + + Ok(results) +} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index e671a7c..4f70d83 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -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"); - } -}