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! {
+
+ }
+ }
+ },
+ 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) -> ();
-}