diff --git a/Cargo.lock b/Cargo.lock index 1d7ab8f..b67a992 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1721,6 +1721,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -3754,6 +3763,7 @@ dependencies = [ "futures", "gloo-storage", "hex", + "itertools 0.13.0", "leptos", "leptos-qr-scanner", "leptos-use", diff --git a/Cargo.toml b/Cargo.toml index 837bd17..5d63635 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ fedimint-mint-client = "0.2.1-rc1" fedimint-ln-client = "0.2.1-rc1" futures = "0.3.28" hex = "0.4.3" +itertools = "0.13.0" leptos = { version = "0.6.5", features = ["csr"] } leptos-use = "0.10.2" leptos-qr-scanner = { git = "https://github.com/elsirion/leptos-qr-scanner", rev = "5830bd6f75d7836189ef1434f71a10222a737a44" } diff --git a/src/components/app.rs b/src/components/app.rs index 4f7ed90..30a1a9c 100644 --- a/src/components/app.rs +++ b/src/components/app.rs @@ -111,6 +111,7 @@ pub fn App() -> impl IntoView { placeholder="invite code".into() submit_label="Join".into() loading=join_action.pending() + default_scan=true /> diff --git a/src/components/create_wallet.rs b/src/components/create_wallet.rs index 988d98c..ba1d3dc 100644 --- a/src/components/create_wallet.rs +++ b/src/components/create_wallet.rs @@ -28,6 +28,7 @@ where +
+
} diff --git a/src/components/joined.rs b/src/components/joined.rs index 6c757a1..88e7f22 100644 --- a/src/components/joined.rs +++ b/src/components/joined.rs @@ -1,6 +1,6 @@ use leptos::*; -use crate::components::{Balance, ReceiveEcash, ReceiveLn, SendEcash, SendLn, TxList}; +use crate::components::{Balance, Receive, Send, TxList}; use crate::context::ClientContext; // @@ -36,20 +36,12 @@ pub fn Joined() -> impl IntoView { view: view! { }, }, MenuItem { - title: "Spend".into(), - view: view! { }, + title: "Send".into(), + view: view! { }, }, MenuItem { - title: "Redeem".into(), - view: view! { }, - }, - MenuItem { - title: "LN Send".into(), - view: view! { }, - }, - MenuItem { - title: "LN Receive".into(), - view: view! { }, + title: "Receive".into(), + view: view! { }, }, ]; diff --git a/src/components/mod.rs b/src/components/mod.rs index cc94870..8fcc24c 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -9,9 +9,13 @@ pub mod ln_receive_form; pub mod loader_icon; pub mod logo; pub mod logo_fedimint; +pub mod protocol_selector; pub mod qrcode; +pub mod receive; pub mod receive_ecash; pub mod receive_ln; +pub mod segmented_button; +pub mod send; pub mod send_ecash; pub mod send_ln; pub mod service_worker; @@ -29,9 +33,13 @@ pub use joined::*; pub use loader_icon::*; pub use logo::*; pub use logo_fedimint::*; +pub use protocol_selector::*; pub use qrcode::*; +pub use receive::*; pub use receive_ecash::*; pub use receive_ln::*; +pub use segmented_button::*; +pub use send::*; pub use send_ecash::*; pub use send_ln::*; pub use submit_button::*; diff --git a/src/components/protocol_selector.rs b/src/components/protocol_selector.rs new file mode 100644 index 0000000..a5132cf --- /dev/null +++ b/src/components/protocol_selector.rs @@ -0,0 +1,45 @@ +use leptos::*; + +use crate::components::SegmentedButton; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Protocol { + OnChain, + Lightning, + ECash, +} + +impl Protocol { + fn from_idx(idx: usize) -> Protocol { + match idx { + 0 => Protocol::OnChain, + 1 => Protocol::Lightning, + 2 => Protocol::ECash, + _ => panic!("Out of bounds"), + } + } + + fn into_idx(self) -> usize { + match self { + Protocol::OnChain => 0, + Protocol::Lightning => 1, + Protocol::ECash => 2, + } + } +} +#[component] +pub fn ProtocolSelector( + #[prop(default = Protocol::Lightning)] active_protocol: Protocol, + on_change: F, +) -> impl IntoView +where + F: Fn(Protocol) + 'static + Copy, +{ + view! { + + } +} diff --git a/src/components/receive.rs b/src/components/receive.rs new file mode 100644 index 0000000..7ece608 --- /dev/null +++ b/src/components/receive.rs @@ -0,0 +1,32 @@ +use leptos::*; + +use crate::components::{Protocol, ProtocolSelector, ReceiveEcash, ReceiveLn}; + +#[component] +pub fn Receive() -> impl IntoView { + const DEFAULT_PROTOCOL: Protocol = Protocol::Lightning; + let (active_protocol, set_active_protocol) = create_signal(DEFAULT_PROTOCOL); + + let active_protocol_view = move || match active_protocol.get() { + Protocol::OnChain => view! { + "TODO" + } + .into_view(), + Protocol::Lightning => view! { + + } + .into_view(), + Protocol::ECash => view! { + + } + .into_view(), + }; + + view! { + + {active_protocol_view} + } +} diff --git a/src/components/receive_ecash.rs b/src/components/receive_ecash.rs index 05cecce..63de8d0 100644 --- a/src/components/receive_ecash.rs +++ b/src/components/receive_ecash.rs @@ -24,6 +24,7 @@ pub fn ReceiveEcash() -> impl IntoView { placeholder="e-cash notes".into() submit_label="Redeem".into() loading=submit_action.pending() + default_scan=true /> {move || diff --git a/src/components/segmented_button.rs b/src/components/segmented_button.rs new file mode 100644 index 0000000..4347bd9 --- /dev/null +++ b/src/components/segmented_button.rs @@ -0,0 +1,57 @@ +use itertools::Itertools; +use leptos::*; + +#[component] +pub fn SegmentedButton( + #[prop(default = 0)] active_idx: usize, + segments: Vec, + on_change: F, +) -> impl IntoView +where + F: Fn(usize) + 'static + Copy, +{ + const ALL_CLASS: &str = "px-4 py-2 focus:outline-none flex-1 text-center"; + const FIRST_CLASS: &str = "rounded-l-full"; + const LAST_CLASS: &str = "rounded-r-full"; + const ACTIVE_CLASS: &str = "text-white bg-blue-500"; + const INACTIVE_CLASS: &str = "text-blue-500 bg-transparent"; + + let (active_idx, set_active_idx) = create_signal(active_idx); + let num_segments = segments.len(); + + let buttons = segments + .into_iter() + .enumerate() + .map(|(idx, name)| { + let class = move || { + let is_first = idx == 0; + let is_last = idx == num_segments - 1; + let is_active = idx == active_idx.get(); + + std::iter::once(ALL_CLASS) + .chain(is_first.then(|| FIRST_CLASS)) + .chain(is_last.then(|| LAST_CLASS)) + .chain(is_active.then(|| ACTIVE_CLASS)) + .chain((!is_active).then(|| INACTIVE_CLASS)) + .join(" ") + }; + view! { + + } + }) + .collect::>(); + + view! { +
+ {buttons} +
+ } +} diff --git a/src/components/send.rs b/src/components/send.rs new file mode 100644 index 0000000..e07ca1d --- /dev/null +++ b/src/components/send.rs @@ -0,0 +1,32 @@ +use leptos::*; + +use crate::components::{Protocol, ProtocolSelector, SendEcash, SendLn}; + +#[component] +pub fn Send() -> impl IntoView { + const DEFAULT_PROTOCOL: Protocol = Protocol::Lightning; + let (active_protocol, set_active_protocol) = create_signal(DEFAULT_PROTOCOL); + + let active_protocol_view = move || match active_protocol.get() { + Protocol::OnChain => view! { + "TODO" + } + .into_view(), + Protocol::Lightning => view! { + + } + .into_view(), + Protocol::ECash => view! { + + } + .into_view(), + }; + + view! { + + {active_protocol_view} + } +} diff --git a/src/components/send_ln.rs b/src/components/send_ln.rs index 5a3e0ac..c449b30 100644 --- a/src/components/send_ln.rs +++ b/src/components/send_ln.rs @@ -24,6 +24,7 @@ pub fn SendLn() -> impl IntoView { placeholder="LN invoice".into() submit_label="Send".into() loading=submit_action.pending() + default_scan=true /> diff --git a/src/components/submit_form.rs b/src/components/submit_form.rs index e027ae1..4c5341e 100644 --- a/src/components/submit_form.rs +++ b/src/components/submit_form.rs @@ -1,6 +1,8 @@ use leptos::ev::KeyboardEvent; +use leptos::html::Form; use leptos::*; use leptos_qr_scanner::Scan; +use leptos_use::use_element_visibility; use crate::components::SubmitButton; @@ -11,6 +13,7 @@ pub fn SubmitForm( description: String, submit_label: String, loading: ReadSignal, + #[prop(default = false)] default_scan: bool, ) -> impl IntoView where F: Fn(String) + 'static + Copy, @@ -20,7 +23,7 @@ where let button_is_disabled = Signal::derive(move || loading.get() || value.get().is_empty()); let scan_disabled = Signal::derive(move || loading.get()); - let (scan, set_scan) = create_signal(false); + let (scan, set_scan) = create_signal(default_scan); let textarea = view! {