diff --git a/Cargo.lock b/Cargo.lock index 0830744..ae30ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anymap2" version = "0.13.0" @@ -91,6 +106,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -101,12 +131,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fnv" version = "1.0.7" @@ -588,6 +636,29 @@ dependencies = [ "itoa", ] +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "implicit-clone" version = "0.4.6" @@ -607,6 +678,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "interactions" version = "0.1.0" @@ -618,6 +701,15 @@ dependencies = [ "todolist", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -640,13 +732,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] -name = "local-storage" -version = "0.1.0" +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" dependencies = [ - "async-trait", - "gloo-storage 0.3.0", - "interactions", - "serde", + "proc-macro2", ] [[package]] @@ -661,6 +752,12 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -670,6 +767,25 @@ dependencies = [ "adler", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -814,6 +930,15 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "pubsub" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943ab27ead5ae69b4a0d28961a089fe55536e6e092856a9b0ae8dfc50275e8c" +dependencies = [ + "threadpool", +] + [[package]] name = "quote" version = "1.0.33" @@ -915,6 +1040,52 @@ dependencies = [ "autocfg", ] +[[package]] +name = "stylist" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684929eeaa18b44296533430c1453f6ea0ebff8cc7182185657fc7887ad5b9d4" +dependencies = [ + "fastrand", + "instant", + "once_cell", + "serde", + "stylist-core", + "stylist-macros", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "stylist-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c59bd4f35e91ac75facd4b904916abddcbfca73ce70674e5babc47617dc50f7" +dependencies = [ + "nom", + "once_cell", + "serde", + "thiserror", + "wasm-bindgen", +] + +[[package]] +name = "stylist-macros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a93326fb80057248f81d95d9c648eab0338f353ceae4263a5e345de836fa9" +dependencies = [ + "itertools", + "litrs", + "log", + "nom", + "proc-macro-error", + "proc-macro2", + "quote", + "stylist-core", + "syn 2.0.38", +] + [[package]] name = "syn" version = "1.0.109" @@ -956,11 +1127,19 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "threadpool" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a63a15230070db3a9f599491c23c4bf38e86bd47c8f2d86ff1f8d3b2e85b0c70" + [[package]] name = "todolist" version = "0.1.0" dependencies = [ + "chrono", "serde", + "thiserror", ] [[package]] @@ -1032,19 +1211,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "ui" -version = "0.1.0" -dependencies = [ - "gloo-console 0.3.0", - "interactions", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "yew", -] - [[package]] name = "unicode-ident" version = "1.0.12" @@ -1144,12 +1310,87 @@ name = "webapp" version = "0.1.0" dependencies = [ "async-trait", + "chrono", + "gloo 0.10.0", + "gloo-console 0.3.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", "interactions", - "local-storage", - "todolist", - "ui", + "js-sys", + "pubsub", + "stylist", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winnow" version = "0.5.19" diff --git a/Cargo.toml b/Cargo.toml index 821892c..7930006 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,5 @@ resolver = "2" members = [ "applications/*", "domains/*", - "drivers/*", "interactions", ] diff --git a/applications/web/Cargo.toml b/applications/web/Cargo.toml index 5c2c74b..ff6cd6f 100644 --- a/applications/web/Cargo.toml +++ b/applications/web/Cargo.toml @@ -7,7 +7,16 @@ edition = "2021" [dependencies] async-trait = "0.1.74" +chrono = "0.4.31" +gloo = "0.10.0" +gloo-console = "0.3.0" +gloo-storage = "0.3.0" +gloo-timers = "0.3.0" interactions = { path = "../../interactions" } -local-storage = { path = "../../drivers/local-storage" } -todolist = { path = "../../domains/todolist" } -ui = { path = "../../drivers/ui" } +js-sys = "0.3.65" +pubsub = "0.2.3" +stylist = "0.13.0" +wasm-bindgen = "0.2.88" +wasm-bindgen-futures = "0.4.38" +web-sys = { version = "0.3.65", features = ["DomTokenList", "Element"] } +yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } diff --git a/applications/web/Trunk.toml b/applications/web/Trunk.toml index 9e9d022..b8dfcdf 100644 --- a/applications/web/Trunk.toml +++ b/applications/web/Trunk.toml @@ -1,8 +1,7 @@ [watch] # Paths to watch. The `build.target`'s parent folder is watched by default. watch = [ - "../../drivers/ui/src", - "../../interactions/src", + "../../", ] [serve] diff --git a/applications/web/index.css b/applications/web/index.css new file mode 100644 index 0000000..b0ca745 --- /dev/null +++ b/applications/web/index.css @@ -0,0 +1,136 @@ +@import url("https://fonts.googleapis.com/css2?family=Caveat&family=VT323&display=swap"); + +main { + display:flex; + flex-direction: column; + height: 100%; + margin: auto; + max-width: 700px; +} + +.countdown { + font-family: "VT323", monospace; + font-size: 4rem; +} + +.sticky-note { + align-items: center; + background-color: #f5f6f8; + /* border-radius: 10px; */ + box-shadow: + /* The top layer shadow */ + 0 1px 1px rgba(0,0,0,0.15), + /* The second layer */ + 0 10px 0 -5px #fff9b1, + /* The second layer shadow */ + 0 10px 1px -4px rgba(0,0,0,0.15), + /* The third layer */ + 0 20px 0 -10px #fff9b1, + /* The third layer shadow */ + 0 20px 1px -9px rgba(0,0,0,0.15); + display: flex; + flex-grow: 1; + font-family: "Caveat", cursive; + font-size: 2rem; + justify-content: center; + padding: 20px; + margin-bottom: 40px; +} + + +/* https://uploads-us-west-2.insided.com/miro-us/attachment/27fae09a-d57c-46fa-8b02-37399c796235.jpg */ +.bg-white { background-color: #f5f6f8; } +.bg-yellow { background-color: #fff9b1; } +.bg-apple-green { background-color: #daf7a1; } +.bg-orange { background-color: #ffc000; } +.bg-lime-green { background-color: #c9df56; } +.bg-peach { background-color: #ff9d48; } +.bg-green { background-color: #b6d7a9; } +.bg-red { background-color: #f16c7f; } +.bg-teal { background-color: #77ccc7; } +.bg-pink { background-color: #eca2c4; } +.bg-cyan { background-color: #6ed8fa; } +.bg-light-pink { background-color: #ffcee0; } +.bg-light-blue { background-color: #b1d3f6; } +.bg-magenta { background-color: #b485dc; } +.bg-indigo { background-color: #8ca0ff; } + +/* Long Press Animation */ +@keyframes grow { + 0% { + transform: scale(1); + } + 100% { + transform: scale(1.75); + } +} + +.longpress { + /* duration | easing-function | delay | iteration-count | direction | fill-mode | play-state | name */ + animation: 2s ease forwards grow; +} + +.longpress-background { + align-items: center; + background-color: white; + clip-path: inset(100% 0% 0% 0%); + display: flex; + font-size: 24px; + height: 100%; + justify-content: center; + position: absolute; + width: 100%; +} + +@keyframes fill-bottom-to-up { + 0% { + clip-path: inset(100% 0% 0% 0%); + } + 100% { + clip-path: inset(0% 0% 0% 0%); + } +} + +.longpress .longpress-background { + animation: 2s ease forwards fill-bottom-to-up; +} + +ion-button.longpress-button { + --box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); +} + +ion-button.longpress-button { + text-transform: none; + /* padding: 14px 2em; */ +} + +.longpress-success { + align-items: center; + background-color: white; + bottom: 0; + clip-path: inset(0% 100% 0% 0%); + display: flex; + font-size: 24px; + justify-content: center; + left: 0; + margin-bottom: calc(-1 * var(--padding-bottom) - 1px); + margin-inline-end: calc(-1 * var(--padding-end)); + margin-inline-start: calc(-1 * var(--padding-start)); + margin-top: calc(-1 * var(--padding-top) - 1px); + position: absolute; + right: 0; + top: 0; +} + +@keyframes fill-left-to-right { + 0% { + clip-path: inset(0% 100% 0% 0%); + } + 100% { + clip-path: inset(0% 0% 0% 0%); + } +} + +.longpress .longpress-success { + animation: 2s ease forwards fill-left-to-right; +} diff --git a/applications/web/index.html b/applications/web/index.html index bd4f30a..945d0a3 100644 --- a/applications/web/index.html +++ b/applications/web/index.html @@ -2,12 +2,14 @@ - TodoList + Task + - + + - - - + + + + + + diff --git a/applications/web/src/app.rs b/applications/web/src/app.rs new file mode 100644 index 0000000..209e685 --- /dev/null +++ b/applications/web/src/app.rs @@ -0,0 +1,20 @@ +use crate::hooks::{RuntimeProvider, TaskStateProvider}; +use crate::screens::TaskScreen; +use yew::prelude::*; + +#[function_component] +pub fn App() -> Html { + html! { + + + + + + } +} + +impl App { + pub fn render() { + yew::Renderer::::new().render(); + } +} diff --git a/applications/web/src/components/countdown.rs b/applications/web/src/components/countdown.rs new file mode 100644 index 0000000..30dfd36 --- /dev/null +++ b/applications/web/src/components/countdown.rs @@ -0,0 +1,39 @@ +use gloo_timers::callback::Interval; +use std::cmp::max; +use yew::prelude::*; + +#[derive(Properties, PartialEq)] +pub struct CountdownProps { + pub seconds: i64, +} + +#[function_component] +pub fn Countdown(props: &CountdownProps) -> Html { + let seconds = use_state(|| props.seconds); + + { + let seconds = seconds.clone(); + use_effect_with((), move |_| { + let mut i = 0; + let interval = Interval::new(1000, move || { + i += 1; + let new_seconds = *seconds.clone() - i; + seconds.set(new_seconds); + }); + || { + interval.forget(); + } + }); + } + + html! { + format_seconds(&seconds) + } +} + +fn format_seconds(seconds: &i64) -> String { + let seconds = max(seconds, &0); + let minutes = seconds / 60; + let seconds = seconds % 60; + format!("{:02}m {:02}s", minutes, seconds) +} diff --git a/applications/web/src/components/long_press_button.rs b/applications/web/src/components/long_press_button.rs new file mode 100644 index 0000000..34e34dd --- /dev/null +++ b/applications/web/src/components/long_press_button.rs @@ -0,0 +1,41 @@ +use yew::prelude::*; + +use crate::hooks::use_long_press; + +#[derive(Properties, PartialEq)] +pub struct LongPressButtonProps { + #[prop_or_default] + pub children: Children, + + #[prop_or("primary".into())] + pub color: AttrValue, + + #[prop_or_default] + pub onclick: Callback, + + #[prop_or("round".into())] + pub shape: AttrValue, + + #[prop_or("large".into())] + pub size: AttrValue, +} + +#[function_component] +pub fn LongPressButton(props: &LongPressButtonProps) -> Html { + let button_ref = use_long_press(props.onclick.clone(), 2_000); + + html! { + +
+ +
+ {props.children.clone()} +
+ } +} diff --git a/applications/web/src/components/long_press_fab.rs b/applications/web/src/components/long_press_fab.rs new file mode 100644 index 0000000..cfe079e --- /dev/null +++ b/applications/web/src/components/long_press_fab.rs @@ -0,0 +1,38 @@ +use yew::prelude::*; + +use crate::hooks::use_long_press; + +#[derive(Properties, PartialEq)] +pub struct LongPressFabProps { + #[prop_or("primary".into())] + pub color: AttrValue, + + #[prop_or(2_000)] + pub duration: u32, + + pub icon: AttrValue, + + #[prop_or_default] + pub onclick: Callback, + + #[prop_or_default] + pub size: Option, +} + +#[function_component] +pub fn LongPressFab(props: &LongPressFabProps) -> Html { + let button_ref = use_long_press(props.onclick.clone(), 2_000); + + html! { + + +
+ +
+
+ } +} diff --git a/applications/web/src/components/mod.rs b/applications/web/src/components/mod.rs new file mode 100644 index 0000000..c7dc0fa --- /dev/null +++ b/applications/web/src/components/mod.rs @@ -0,0 +1,7 @@ +pub mod countdown; +pub mod long_press_button; +pub mod long_press_fab; + +pub use countdown::*; +pub use long_press_button::*; +pub use long_press_fab::*; diff --git a/applications/web/src/hooks/mod.rs b/applications/web/src/hooks/mod.rs new file mode 100644 index 0000000..ee7cfce --- /dev/null +++ b/applications/web/src/hooks/mod.rs @@ -0,0 +1,7 @@ +pub mod use_longpress; +pub mod use_runtime; +pub mod use_task_state; + +pub use use_longpress::*; +pub use use_runtime::*; +pub use use_task_state::*; diff --git a/applications/web/src/hooks/use_longpress.rs b/applications/web/src/hooks/use_longpress.rs new file mode 100644 index 0000000..5ed6a7e --- /dev/null +++ b/applications/web/src/hooks/use_longpress.rs @@ -0,0 +1,74 @@ +use gloo_timers::callback::Timeout; +use wasm_bindgen::prelude::*; +use web_sys::HtmlElement; +use yew::prelude::*; + +#[hook] +pub fn use_long_press(onclick: Callback, duration_millis: u32) -> NodeRef { + // Refs + let node_ref = use_node_ref(); + + // States + let timeout = use_state(|| None); + + // Callbacks + let onpress = { + let node_ref = node_ref.clone(); + let timeout = timeout.clone(); + Callback::from(move |_: TouchEvent| { + let element = node_ref + .cast::() + .expect("Unable to cast to HtmlElement"); + + element.class_list().add_1("longpress").unwrap(); + + let onclick = onclick.clone(); + timeout.set(Some(Timeout::new(duration_millis, move || { + onclick.emit(MouseEvent::new("click").unwrap()); + }))); + }) + }; + let onrelease = { + // let loader = loader.clone(); + let node_ref = node_ref.clone(); + let timeout = timeout.clone(); + Callback::from(move |_: TouchEvent| { + let element = node_ref + .cast::() + .expect("Unable to cast to HtmlElement"); + element.class_list().remove_1("longpress").unwrap(); + + timeout.set(None); + }) + }; + + // Closures + let press_closure = use_memo((), move |_| { + Closure::::new(move || { + onpress.emit(TouchEvent::new("touchstart").unwrap()); + }) + }); + let release_closure = use_memo((), move |_| { + Closure::::new(move || { + onrelease.emit(TouchEvent::new("touchend").unwrap()); + }) + }); + + // Effects + use_effect_with(node_ref.clone(), move |node_ref| { + let element = node_ref + .cast::() + .expect("Unable to cast to HtmlElement"); + + element.set_onclick(None); + element.set_onmousedown(Some((*press_closure.clone()).as_ref().unchecked_ref())); + element.set_onmouseleave(Some((*release_closure.clone()).as_ref().unchecked_ref())); + element.set_onmouseup(Some((*release_closure.clone()).as_ref().unchecked_ref())); + element.set_ontouchend(Some((*release_closure.clone()).as_ref().unchecked_ref())); + element.set_ontouchstart(Some((*press_closure.clone()).as_ref().unchecked_ref())); + + || () + }); + + node_ref +} diff --git a/applications/web/src/hooks/use_runtime.rs b/applications/web/src/hooks/use_runtime.rs new file mode 100644 index 0000000..f999deb --- /dev/null +++ b/applications/web/src/hooks/use_runtime.rs @@ -0,0 +1,30 @@ +use crate::runtime::Runtime; +use yew::prelude::*; + +impl PartialEq for Runtime { + fn eq(&self, _: &Self) -> bool { + true + } +} + +#[derive(Debug, PartialEq, Properties)] +pub struct RuntimeProviderProps { + #[prop_or_default] + pub children: Html, +} + +#[function_component] +pub fn RuntimeProvider(props: &RuntimeProviderProps) -> Html { + let runtime = Runtime {}; + + html! { + context={runtime}> + {props.children.clone()} + > + } +} + +#[hook] +pub fn use_runtime() -> Runtime { + use_context::().expect("no ctx found") +} diff --git a/applications/web/src/hooks/use_task_state.rs b/applications/web/src/hooks/use_task_state.rs new file mode 100644 index 0000000..e1a2542 --- /dev/null +++ b/applications/web/src/hooks/use_task_state.rs @@ -0,0 +1,26 @@ +use interactions::presenters::TaskState; +use yew::prelude::*; + +pub type TaskStateHandle = UseStateHandle; + +#[derive(Debug, PartialEq, Properties)] +pub struct TaskStateProviderProps { + #[prop_or_default] + pub children: Html, +} + +#[function_component] +pub fn TaskStateProvider(props: &TaskStateProviderProps) -> Html { + let msg = use_state(TaskState::default); + + html! { + context={msg}> + {props.children.clone()} + > + } +} + +#[hook] +pub fn use_task_state() -> TaskStateHandle { + use_context::().expect("no ctx found") +} diff --git a/applications/web/src/ionic.rs b/applications/web/src/ionic.rs new file mode 100644 index 0000000..aa6ee4c --- /dev/null +++ b/applications/web/src/ionic.rs @@ -0,0 +1,30 @@ +#![allow(unused_imports)] +#![allow(clippy::all)] +use wasm_bindgen::prelude::*; +use web_sys::*; + +#[wasm_bindgen] +extern "C" { + /// HTMLIonModalElement + #[wasm_bindgen (extends = HtmlElement , extends = Element , extends = Node , extends = EventTarget , extends = :: js_sys :: Object , js_name = HTMLIonModalElement , typescript_type = "HTMLIonModalElement")] + pub type HTMLIonModalElement; + + #[wasm_bindgen (structural , method , js_class = "HTMLIonModalElement" , js_name = dismiss)] + pub fn dismiss(this: &HTMLIonModalElement, value: JsValue, role: Option<&str>) -> bool; + + #[wasm_bindgen (structural , method , js_class = "HTMLIonModalElement" , js_name = present)] + pub async fn present(this: &HTMLIonModalElement); +} + +#[wasm_bindgen] +extern "C" { + /// HTMLIonTextareaElement + #[wasm_bindgen (extends = HtmlElement , extends = Element , extends = Node , extends = EventTarget , extends = :: js_sys :: Object , js_name = HTMLIonTextareaElement , typescript_type = "HTMLIonTextareaElement")] + pub type HTMLIonTextareaElement; + + #[wasm_bindgen (structural , method , js_class = "HTMLIonTextareaElement" , js_name = setFocus)] + pub async fn set_focus(this: &HTMLIonTextareaElement); + + #[wasm_bindgen (structural , method , js_class = "HTMLIonTextareaElement" , js_name = getInputElement)] + pub async fn get_input_element(this: &HTMLIonTextareaElement) -> JsValue; +} diff --git a/applications/web/src/main.rs b/applications/web/src/main.rs index a104874..8ac363f 100644 --- a/applications/web/src/main.rs +++ b/applications/web/src/main.rs @@ -1,20 +1,12 @@ +mod app; +mod components; +mod hooks; +mod ionic; mod runtime; +mod screens; -use interactions::presenters::CreateTaskForm; -use runtime::Runtime; -use std::rc::Rc; -use ui::app::{render, AppContext}; +use app::App; fn main() { - let runtime = Rc::new(Runtime::new()); - - let create_task_form = CreateTaskForm { - runtime: runtime.clone(), - }; - - let app_context = AppContext { - create_task_form: Rc::new(create_task_form), - }; - - render(app_context); + App::render(); } diff --git a/applications/web/src/runtime.rs b/applications/web/src/runtime.rs index a4348b9..f080ffe 100644 --- a/applications/web/src/runtime.rs +++ b/applications/web/src/runtime.rs @@ -1,25 +1,49 @@ -use async_trait::async_trait; -use interactions::services::Store; -use local_storage::LocalStore; +use gloo_storage::{LocalStorage, Storage}; +use interactions::commands::TaskCommand; +use interactions::ports::{CurrentTaskRepository, EventStore, SnapshotRepository}; +use interactions::presenters::UseTask; +use interactions::queries::TaskQuery; +use interactions::todolist::{CurrentTask, Event, Snapshot}; -pub struct Runtime { - event_store: LocalStore, +#[derive(Clone, Default)] +pub struct Runtime {} + +#[async_trait::async_trait] +impl CurrentTaskRepository for Runtime { + async fn get(&self) -> Option { + LocalStorage::get::("tasks").ok() + } + async fn save(&self, value: CurrentTask) -> () { + LocalStorage::set("tasks", value).unwrap(); + } } -impl Runtime { - pub fn new() -> Self { - Self { - event_store: LocalStore::new("events".to_string()), - } +#[async_trait::async_trait] +impl EventStore for Runtime { + async fn pull(&self) -> Option> { + LocalStorage::get::>("events").ok() + } + async fn push(&self, new_events: Vec) -> () { + let mut events = self.pull().await.unwrap_or_default(); + events.extend(new_events); + LocalStorage::set("events", &events).unwrap(); } } -#[async_trait] -impl Store for Runtime { - async fn pull(&self) -> Vec { - self.event_store.pull().await +#[async_trait::async_trait] +impl SnapshotRepository for Runtime +where + Runtime: EventStore, +{ + async fn get(&self) -> Option { + // LocalStorage::get::("snapshot").ok() + EventStore::pull(self).await.map(|events| events.into()) } - async fn push(&self, new_events: Vec) -> () { - self.event_store.push(new_events).await + async fn save(&self, value: Snapshot) -> () { + LocalStorage::set("snapshot", value).unwrap(); } } + +impl TaskCommand for Runtime {} +impl TaskQuery for Runtime {} +impl UseTask for Runtime {} diff --git a/applications/web/src/screens/mod.rs b/applications/web/src/screens/mod.rs new file mode 100644 index 0000000..c92e6e6 --- /dev/null +++ b/applications/web/src/screens/mod.rs @@ -0,0 +1,3 @@ +pub mod task_screen; + +pub use task_screen::*; diff --git a/applications/web/src/screens/task_screen/mod.rs b/applications/web/src/screens/task_screen/mod.rs new file mode 100644 index 0000000..64213fb --- /dev/null +++ b/applications/web/src/screens/task_screen/mod.rs @@ -0,0 +1,27 @@ +mod task_buttons; +mod task_create_button; +mod task_error; +mod task_sticky_note; + +use crate::hooks::TaskStateProvider; +use task_buttons::TaskButtons; +use task_error::TaskError; +use task_sticky_note::TaskStickyNote; +use yew::prelude::*; + +#[function_component] +pub fn TaskScreen() -> Html { + html! { + + + +
+ + + +
+
+
+
+ } +} diff --git a/applications/web/src/screens/task_screen/task_buttons.rs b/applications/web/src/screens/task_screen/task_buttons.rs new file mode 100644 index 0000000..3853c14 --- /dev/null +++ b/applications/web/src/screens/task_screen/task_buttons.rs @@ -0,0 +1,75 @@ +use super::task_create_button::TaskCreateButton; +use crate::components::{Countdown, LongPressButton, LongPressFab}; +use crate::hooks::{use_runtime, use_task_state}; +use interactions::{presenters::UseTask, todolist::CurrentTask}; +use yew::prelude::*; + +#[function_component] +pub fn TaskButtons() -> Html { + let runtime = use_runtime(); + let task_state = use_task_state(); + + let stop = { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + Callback::from(move |_: MouseEvent| { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::stop(&runtime, &task_state).await); + }); + }) + }; + + let complete = { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + Callback::from(move |_: MouseEvent| { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::complete(&runtime, &task_state).await); + }); + }) + }; + + let skip = { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + Callback::from(move |_: MouseEvent| { + let task_state = task_state.clone(); + let runtime = runtime.clone(); + + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::skip(&runtime, &task_state).await); + }); + }) + }; + + if let Some(CurrentTask::InProgress { expires_in, .. }) = &task_state.current_task { + html! { +
+ +
+ + + +
+ +
+ } + } else { + html! { +
+
+ +
+
+ } + } +} diff --git a/applications/web/src/screens/task_screen/task_create_button.rs b/applications/web/src/screens/task_screen/task_create_button.rs new file mode 100644 index 0000000..4d87f3a --- /dev/null +++ b/applications/web/src/screens/task_screen/task_create_button.rs @@ -0,0 +1,109 @@ +use crate::hooks::{use_runtime, use_task_state}; +use crate::ionic::*; +use interactions::presenters::UseTask; +use wasm_bindgen::JsValue; +use web_sys::HtmlTextAreaElement; +use yew::prelude::*; + +#[function_component] +pub fn TaskCreateButton() -> Html { + let modal_ref = use_node_ref(); + let textarea_ref = use_node_ref(); + + let runtime = use_runtime(); + let task_state = use_task_state(); + + let open_modal = { + let modal_ref = modal_ref.clone(); + let textarea_ref = textarea_ref.clone(); + + Callback::from(move |_| { + let modal_ref = modal_ref.clone(); + let textarea_ref = textarea_ref.clone(); + wasm_bindgen_futures::spawn_local(async move { + modal_ref + .cast::() + .expect("no modal found") + .present() + .await; + textarea_ref + .cast::() + .expect("no modal found") + .set_focus() + .await; + }) + }) + }; + + let close_modal = { + let modal_ref = modal_ref.clone(); + + Callback::from(move |_e| { + modal_ref + .cast::() + .expect("no modal found") + .dismiss(JsValue::undefined(), None); + }) + }; + + let save = { + let task_state = task_state.clone(); + let close_modal = close_modal.clone(); + let runtime = runtime.clone(); + let textarea_ref = textarea_ref.clone(); + + Callback::from(move |e| { + let task_state = task_state.clone(); + let close_modal = close_modal.clone(); + let runtime = runtime.clone(); + + let description = textarea_ref + .cast::() + .expect("no modal found") + .value(); + + let textarea_ref = textarea_ref.clone(); + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::add(&runtime, &task_state, description).await); + let textarea: HtmlTextAreaElement = textarea_ref + .cast::() + .expect("no modal found") + .get_input_element() + .await + .into(); + textarea.set_value(""); + close_modal.emit(e) + }); + }) + }; + + html! { + <> + + + + + + + + {"Cancel"} + + + {"Save"} + + + + + + + + + + } +} diff --git a/applications/web/src/screens/task_screen/task_error.rs b/applications/web/src/screens/task_screen/task_error.rs new file mode 100644 index 0000000..dac4f6d --- /dev/null +++ b/applications/web/src/screens/task_screen/task_error.rs @@ -0,0 +1,27 @@ +use crate::hooks::use_task_state; + +use yew::prelude::*; + +#[function_component] +pub fn TaskError() -> Html { + let is_open = use_state(|| false); + let task_state = use_task_state(); + + { + let error = task_state.error.clone(); + let is_open = is_open.clone(); + use_effect_with(error.clone(), move |error| { + if error.is_some() { + is_open.set(true); + } + || () + }); + } + + let error_msg = task_state.error.clone().unwrap_or("".to_string()); + let is_open_attr = is_open.clone().to_string(); + + html! { + + } +} diff --git a/applications/web/src/screens/task_screen/task_sticky_note.rs b/applications/web/src/screens/task_screen/task_sticky_note.rs new file mode 100644 index 0000000..01a0e1e --- /dev/null +++ b/applications/web/src/screens/task_screen/task_sticky_note.rs @@ -0,0 +1,65 @@ +use crate::components::LongPressButton; +use crate::hooks::{use_runtime, use_task_state}; +use interactions::{presenters::UseTask, todolist::CurrentTask}; +use yew::prelude::*; + +#[function_component] +pub fn TaskStickyNote() -> Html { + let runtime = use_runtime(); + let task_state = use_task_state(); + + { + let runtime = runtime.clone(); + let task_state = task_state.clone(); + use_effect_with((), move |_| { + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::get_current(&runtime, &task_state).await); + }); + || () + }); + } + + let start = { + let runtime = runtime.clone(); + let task_state = task_state.clone(); + Callback::from(move |_| { + let runtime = runtime.clone(); + let task_state = task_state.clone(); + wasm_bindgen_futures::spawn_local(async move { + task_state.set(UseTask::start(&runtime, &task_state).await); + }); + }) + }; + + match &task_state.current_task { + Some(task) => match task { + CurrentTask::None => html! { +
+

+ {"Nothing here yet..."} +
+ {"Please add a task with the button below."} +

+
+ }, + CurrentTask::Ready => html! { +
+ + {"Start"} + + +
+ }, + CurrentTask::InProgress { description, .. } => { + html! { +
+

{description}

+
+ } + } + }, + None => html! { + + }, + } +} diff --git a/domains/todolist/Cargo.toml b/domains/todolist/Cargo.toml index f737bda..8b8b6a5 100644 --- a/domains/todolist/Cargo.toml +++ b/domains/todolist/Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0.130", features = ["derive"] } +thiserror = "1.0.50" diff --git a/domains/todolist/src/errors.rs b/domains/todolist/src/errors.rs new file mode 100644 index 0000000..a426be9 --- /dev/null +++ b/domains/todolist/src/errors.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Empty backlog")] + EmptyBacklog, + + #[error("Task already started")] + TaskAlreadyStarted, + + #[error("Task not started")] + TaskNotStarted, +} diff --git a/domains/todolist/src/events.rs b/domains/todolist/src/events.rs index a79d062..ff7651e 100644 --- a/domains/todolist/src/events.rs +++ b/domains/todolist/src/events.rs @@ -1,7 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub enum Event { TaskAdded { description: String }, - TaskCompleted { index: usize }, + TaskCompleted, + TaskDeleted, + TaskPaused, + TaskSkipped, + TaskStarted { at: chrono::DateTime }, } diff --git a/domains/todolist/src/lib.rs b/domains/todolist/src/lib.rs index e208ceb..6c358ce 100644 --- a/domains/todolist/src/lib.rs +++ b/domains/todolist/src/lib.rs @@ -1,7 +1,13 @@ +mod errors; mod events; mod messages; mod projections; +mod scalars; +mod snapshot; +pub use errors::*; pub use events::*; pub use messages::*; pub use projections::*; +pub use scalars::*; +pub use snapshot::*; diff --git a/domains/todolist/src/messages.rs b/domains/todolist/src/messages.rs index 55fd65d..3c905b6 100644 --- a/domains/todolist/src/messages.rs +++ b/domains/todolist/src/messages.rs @@ -1,20 +1,8 @@ -use crate::events::Event; - pub enum Message { AddTask { description: String }, - CompleteTask { index: usize }, -} - -#[derive(Debug)] -pub enum Error { - TaskNotFound, -} - -impl Message { - pub fn send(self) -> Result, Error> { - match self { - Message::AddTask { description } => Ok(vec![Event::TaskAdded { description }]), - Message::CompleteTask { index } => Ok(vec![Event::TaskCompleted { index }]), - } - } + CompleteTask, + DeleteTask, + PauseTask, + SkipTask, + StartTask, } diff --git a/domains/todolist/src/projections.rs b/domains/todolist/src/projections.rs index be939a8..77c460c 100644 --- a/domains/todolist/src/projections.rs +++ b/domains/todolist/src/projections.rs @@ -1,40 +1,30 @@ +use crate::{Seconds, Snapshot, State}; use serde::{Deserialize, Serialize}; -use crate::events::Event; - -#[derive(Default, Serialize, Deserialize)] -pub struct TodoList { - pub tasks: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct Task { - pub description: String, - pub done: bool, +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] +pub enum CurrentTask { + #[default] + None, + Ready, + InProgress { + description: String, + expires_in: Seconds, + }, } -impl TodoList { - pub fn apply(&mut self, events: Vec) { - for event in events.iter() { - match event { - Event::TaskAdded { description } => { - self.tasks.push(Task { - description: description.clone(), - done: false, - }); - } - Event::TaskCompleted { index } => { - self.tasks[*index].done = true; - } +impl From for CurrentTask { + fn from(snapshot: Snapshot) -> Self { + if let Some(current) = snapshot.backlog.front() { + match snapshot.state { + State::Idle => CurrentTask::Ready, + State::Paused { .. } => CurrentTask::Ready, + State::Started { expires_at } => CurrentTask::InProgress { + description: current.clone(), + expires_in: Seconds((expires_at - chrono::Utc::now()).num_seconds()), + }, } + } else { + CurrentTask::None } } } - -impl From> for TodoList { - fn from(events: Vec) -> Self { - let mut todo_list = TodoList::default(); - todo_list.apply(events); - todo_list - } -} diff --git a/domains/todolist/src/scalars.rs b/domains/todolist/src/scalars.rs new file mode 100644 index 0000000..95a1143 --- /dev/null +++ b/domains/todolist/src/scalars.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Seconds(pub i64); diff --git a/domains/todolist/src/snapshot.rs b/domains/todolist/src/snapshot.rs new file mode 100644 index 0000000..7a5b2ac --- /dev/null +++ b/domains/todolist/src/snapshot.rs @@ -0,0 +1,108 @@ +use crate::{errors::Error, Event, Message, Seconds}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Snapshot { + pub backlog: VecDeque, + pub state: State, +} + +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] +pub enum State { + #[default] + Idle, + Started { + expires_at: chrono::DateTime, + }, + Paused { + remaining: Seconds, + }, +} + +impl Snapshot { + pub fn apply(&mut self, events: Vec) { + for event in events.iter() { + match event { + Event::TaskAdded { description } => { + self.backlog.push_back(description.clone()); + } + Event::TaskCompleted => { + self.backlog.pop_front(); + self.state = State::Idle; + } + Event::TaskDeleted => { + self.backlog.pop_front(); + self.state = State::Idle; + } + Event::TaskPaused => { + if let State::Started { expires_at } = self.state { + self.state = State::Paused { + remaining: Seconds((expires_at - chrono::Utc::now()).num_seconds()), + }; + } + } + Event::TaskSkipped => { + self.backlog.rotate_left(1); + self.state = State::Idle; + } + Event::TaskStarted { at } => { + let duration = match &self.state { + State::Paused { remaining } => chrono::Duration::seconds(remaining.0), + _ => chrono::Duration::minutes(60), + }; + self.state = State::Started { + expires_at: *at + duration, + }; + } + } + } + } + + pub fn send(&self, message: Message) -> Result, Error> { + match message { + Message::AddTask { description } => Ok(vec![Event::TaskAdded { description }]), + Message::CompleteTask => match self.state { + State::Started { .. } => Ok(vec![ + Event::TaskCompleted, + Event::TaskStarted { + at: chrono::Utc::now(), + }, + ]), + _ => Err(Error::TaskNotStarted), + }, + Message::DeleteTask => match self.state { + State::Started { .. } => Ok(vec![Event::TaskDeleted]), + _ => Err(Error::TaskNotStarted), + }, + Message::PauseTask => match self.state { + State::Started { .. } => Ok(vec![Event::TaskPaused]), + _ => Err(Error::TaskNotStarted), + }, + Message::SkipTask => match self.state { + State::Started { .. } => Ok(vec![Event::TaskSkipped]), + _ => Err(Error::TaskNotStarted), + }, + Message::StartTask => match self.state { + State::Idle => { + if !self.backlog.is_empty() { + Ok(vec![Event::TaskStarted { + at: chrono::Utc::now(), + }]) + } else { + Err(Error::EmptyBacklog) + } + } + _ => Err(Error::TaskAlreadyStarted), + }, + } + } +} + +impl From> for Snapshot { + fn from(events: Vec) -> Self { + let mut snapshot = Snapshot::default(); + snapshot.apply(events); + snapshot + } +} diff --git a/domains/todolist/tests/todolist.rs b/domains/todolist/tests/todolist.rs index ec06c2d..39b99c7 100644 --- a/domains/todolist/tests/todolist.rs +++ b/domains/todolist/tests/todolist.rs @@ -2,7 +2,7 @@ // #[test] // fn test_add_task() { -// let mut todo_list = TodoList::default(); +// let mut todo_list = Task::default(); // let cmd = Command::AddTask { // description: "Test task".to_string(), // }; @@ -16,7 +16,7 @@ // #[test] // fn test_complete_task() { -// let mut todo_list = TodoList::default(); +// let mut todo_list = Task::default(); // let cmd_add = Command::AddTask { // description: "Test task".to_string(), // }; @@ -33,7 +33,7 @@ // #[test] // fn test_handle() { -// let todo_list = TodoList::default(); +// let todo_list = Task::default(); // // Testing AddTask command // match todo_list.handle(Command::AddTask { @@ -66,7 +66,7 @@ // #[test] // fn test_apply() { -// let mut todo_list = TodoList::default(); +// let mut todo_list = Task::default(); // let events = vec![ // Event::TaskAdded { diff --git a/drivers/local-storage/.gitignore b/drivers/local-storage/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/drivers/local-storage/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/drivers/local-storage/Cargo.lock b/drivers/local-storage/Cargo.lock deleted file mode 100644 index ef52fb7..0000000 --- a/drivers/local-storage/Cargo.lock +++ /dev/null @@ -1,202 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "application" -version = "0.1.0" -dependencies = [ - "async-trait", - "serde", - "serde_json", - "todolist", -] - -[[package]] -name = "async-trait" -version = "0.1.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "js-sys" -version = "0.3.64" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "local-storage" -version = "0.1.0" -dependencies = [ - "application", - "async-trait", - "serde_json", - "web-sys", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "proc-macro2" -version = "1.0.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "serde" -version = "1.0.188" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.188" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "syn" -version = "2.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "todolist" -version = "0.1.0" -dependencies = [ - "serde", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" - -[[package]] -name = "web-sys" -version = "0.3.64" -dependencies = [ - "js-sys", - "wasm-bindgen", -] diff --git a/drivers/local-storage/Cargo.toml b/drivers/local-storage/Cargo.toml deleted file mode 100644 index 88d31d5..0000000 --- a/drivers/local-storage/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "local-storage" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -async-trait = "0.1.73" -gloo-storage = "0.3.0" -interactions = { path = "../../interactions" } -serde = { version = "1.0.130", features = ["derive"] } diff --git a/drivers/local-storage/src/lib.rs b/drivers/local-storage/src/lib.rs deleted file mode 100644 index b379eb2..0000000 --- a/drivers/local-storage/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -use async_trait::async_trait; -use gloo_storage::{LocalStorage, Storage}; -use interactions::services::Store; -use serde::de::DeserializeOwned; -use serde::Serialize; - -pub struct LocalStore { - key: String, - phantom: std::marker::PhantomData, -} - -impl LocalStore { - pub fn new(key: String) -> Self { - Self { - key, - phantom: std::marker::PhantomData, - } - } -} - -#[async_trait] -impl Store for LocalStore -where - A: Serialize + DeserializeOwned + Sync + Send, -{ - async fn pull(&self) -> Vec { - LocalStorage::get::>(self.key.clone()).unwrap() - } - - async fn push(&self, new_events: Vec) -> () { - let mut events = self.pull().await; - events.extend(new_events); - LocalStorage::set(self.key.clone(), &events).unwrap(); - } -} diff --git a/drivers/ui/Cargo.toml b/drivers/ui/Cargo.toml deleted file mode 100644 index 0828ac3..0000000 --- a/drivers/ui/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "ui" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -gloo-console = "0.3.0" -interactions = { path = "../../interactions" } -js-sys = "0.3.65" -wasm-bindgen = "0.2.88" -wasm-bindgen-futures = "0.4.38" -web-sys = "0.3.65" -yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } diff --git a/drivers/ui/src/app.rs b/drivers/ui/src/app.rs deleted file mode 100644 index 77b2657..0000000 --- a/drivers/ui/src/app.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::home::Home; -use interactions::presenters::CreateTaskForm; -use std::rc::Rc; -use yew::prelude::*; - -#[derive(Properties, Clone)] -pub struct AppContext { - pub create_task_form: Rc, -} - -impl PartialEq for AppContext { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -#[function_component] -pub fn App(context: &AppContext) -> Html { - html! { - context={(*context).clone()}> - - > - } -} - -pub fn render(ctx: AppContext) { - yew::Renderer::::with_props(ctx).render(); -} diff --git a/drivers/ui/src/create_task_modal.rs b/drivers/ui/src/create_task_modal.rs deleted file mode 100644 index a9255bc..0000000 --- a/drivers/ui/src/create_task_modal.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::app::AppContext; -use crate::ionic::*; -use wasm_bindgen::JsValue; -use yew::prelude::*; - -#[function_component] -pub fn CreateTaskModal() -> Html { - let modal_ref = use_node_ref(); - - let presenter = use_context::() - .expect("no ctx found") - .create_task_form; - - let close_modal = { - let modal_ref = modal_ref.clone(); - - Callback::from(move |_| { - modal_ref - .cast::() - .expect("no modal found") - .dismiss(JsValue::undefined(), None); - }) - }; - - html! { - <> - {&presenter.title()} - - - - - {"Cancel"} - - {"Welcome"} - - {"Confirm"} - - - - - - - - - - - } -} diff --git a/drivers/ui/src/home.rs b/drivers/ui/src/home.rs deleted file mode 100644 index 59a1389..0000000 --- a/drivers/ui/src/home.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::create_task_modal::CreateTaskModal; -use yew::prelude::*; - -#[function_component] -pub fn Home() -> Html { - html! { - <> - - - {"Hello!"} - - - - - - - - - } -} diff --git a/drivers/ui/src/ionic.rs b/drivers/ui/src/ionic.rs deleted file mode 100644 index 8906d3a..0000000 --- a/drivers/ui/src/ionic.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![allow(unused_imports)] -#![allow(clippy::all)] -use wasm_bindgen::prelude::*; -use web_sys::*; -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen (extends = HtmlElement , extends = Element , extends = Node , extends = EventTarget , extends = :: js_sys :: Object , js_name = HTMLIonModalElement , typescript_type = "HTMLIonModalElement")] - pub type HTMLIonModalElement; - - #[wasm_bindgen (structural , method , js_class = "HTMLIonModalElement" , js_name = dismiss)] - pub fn dismiss(this: &HTMLIonModalElement, value: JsValue, role: Option<&str>) -> bool; -} diff --git a/drivers/ui/src/lib.rs b/drivers/ui/src/lib.rs deleted file mode 100644 index a587226..0000000 --- a/drivers/ui/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod app; -mod create_task_modal; -mod home; -mod ionic; diff --git a/interactions/src/commands.rs b/interactions/src/commands.rs index 8beedf1..88076b7 100644 --- a/interactions/src/commands.rs +++ b/interactions/src/commands.rs @@ -1,17 +1,16 @@ -use std::rc::Rc; +use crate::ports::{CurrentTaskRepository, EventStore, SnapshotRepository}; -use crate::services::Store; +#[async_trait::async_trait] +pub trait TaskCommand: CurrentTaskRepository + EventStore + SnapshotRepository { + async fn send(&self, message: todolist::Message) -> Result<(), todolist::Error> { + let mut snapshot = SnapshotRepository::get(self).await.unwrap_or_default(); + let new_events = snapshot.send(message)?; + snapshot.apply(new_events.clone()); -pub async fn create_task( - runtime: Rc>, - description: String, -) -> Result<(), todolist::Error> { - let event_store: &dyn Store = runtime.as_ref(); - // let todolist: todolist::TodoList = ::pull(runtime.as_ref()).await.into(); + EventStore::push(self, new_events).await; + SnapshotRepository::save(self, snapshot.clone()).await; + CurrentTaskRepository::save(self, snapshot.into()).await; - let events = todolist::Message::AddTask { description }.send()?; - - event_store.push(events).await; - - Ok(()) + Ok(()) + } } diff --git a/interactions/src/lib.rs b/interactions/src/lib.rs index 9e43a79..52bebae 100644 --- a/interactions/src/lib.rs +++ b/interactions/src/lib.rs @@ -1,3 +1,8 @@ pub mod commands; +pub mod ports; pub mod presenters; -pub mod services; +pub mod queries; + +pub mod todolist { + pub use todolist::*; +} diff --git a/interactions/src/ports.rs b/interactions/src/ports.rs new file mode 100644 index 0000000..c92a566 --- /dev/null +++ b/interactions/src/ports.rs @@ -0,0 +1,24 @@ +#[async_trait::async_trait] +pub trait EventStore { + async fn pull(&self) -> Option>; + async fn push(&self, events: Vec); +} + +#[async_trait::async_trait] +pub trait SnapshotRepository { + async fn get(&self) -> Option; + async fn save(&self, value: todolist::Snapshot); +} + +#[async_trait::async_trait] +pub trait CurrentTaskRepository { + async fn get(&self) -> Option; + async fn save(&self, value: todolist::CurrentTask); +} + +pub trait Observable { + fn mutate(&self, new_state: T); + fn observe(&self, subscriber: F) + where + F: Fn(&T); +} diff --git a/interactions/src/presenters.rs b/interactions/src/presenters.rs index 5b8f5e4..103d0fc 100644 --- a/interactions/src/presenters.rs +++ b/interactions/src/presenters.rs @@ -1,19 +1,95 @@ -use std::rc::Rc; +use todolist::CurrentTask; -use crate::{commands::create_task, services::Store}; +use crate::{commands::TaskCommand, queries::TaskQuery}; -pub struct CreateTaskForm { - pub runtime: Rc>, +#[derive(Clone, Default, PartialEq)] +pub struct TaskState { + pub current_task: Option, + pub error: Option, } -impl CreateTaskForm { - pub async fn onsubmit(&self, description: String) { - create_task(self.runtime.clone(), description) - .await - .unwrap(); +#[async_trait::async_trait] +pub trait UseTask: TaskCommand + TaskQuery { + async fn add(&self, state: &TaskState, description: String) -> TaskState { + if let Err(error) = + TaskCommand::send(self, todolist::Message::AddTask { description }).await + { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } } - pub fn title(&self) -> String { - "Create Task".to_string() + async fn complete(&self, state: &TaskState) -> TaskState { + if let Err(error) = TaskCommand::send(self, todolist::Message::CompleteTask).await { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } + } + + async fn delete(&self, state: &TaskState) -> TaskState { + if let Err(error) = TaskCommand::send(self, todolist::Message::DeleteTask).await { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } + } + + fn dismiss_error(&self, state: &TaskState) -> TaskState { + TaskState { + error: None, + ..state.clone() + } + } + + async fn get_current(&self, state: &TaskState) -> TaskState { + let current_task = Some(TaskQuery::get_current_task(self).await); + TaskState { + current_task, + ..state.clone() + } + } + + async fn skip(&self, state: &TaskState) -> TaskState { + if let Err(error) = TaskCommand::send(self, todolist::Message::SkipTask).await { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } + } + + async fn start(&self, state: &TaskState) -> TaskState { + if let Err(error) = TaskCommand::send(self, todolist::Message::StartTask).await { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } + } + + async fn stop(&self, state: &TaskState) -> TaskState { + if let Err(error) = TaskCommand::send(self, todolist::Message::PauseTask).await { + TaskState { + error: Some(error.to_string()), + ..state.clone() + } + } else { + self.get_current(state).await + } } } diff --git a/interactions/src/queries.rs b/interactions/src/queries.rs new file mode 100644 index 0000000..709871c --- /dev/null +++ b/interactions/src/queries.rs @@ -0,0 +1,9 @@ +use crate::ports::CurrentTaskRepository; +use todolist::CurrentTask; + +#[async_trait::async_trait] +pub trait TaskQuery: CurrentTaskRepository { + async fn get_current_task(&self) -> CurrentTask { + CurrentTaskRepository::get(self).await.unwrap_or_default() + } +} diff --git a/interactions/src/services.rs b/interactions/src/services.rs deleted file mode 100644 index bc21567..0000000 --- a/interactions/src/services.rs +++ /dev/null @@ -1,12 +0,0 @@ -use async_trait::async_trait; -use serde::de::DeserializeOwned; -use serde::Serialize; - -#[async_trait] -pub trait Store -where - A: Serialize + DeserializeOwned + Sync + Send, -{ - async fn pull(&self) -> Vec; - async fn push(&self, events: Vec) -> (); -}