Skip to content

Commit 68de4d5

Browse files
authored
Merge pull request #136 from rambit-systems/push-skwozvtvnnvp
Incremental UI Push #20
2 parents c2b4e5b + 0a72e99 commit 68de4d5

File tree

9 files changed

+409
-223
lines changed

9 files changed

+409
-223
lines changed

crates/site-app/src/components/data_table.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,15 @@ pub fn DataTableRefreshButton<
3838

3939
#[component]
4040
pub fn TableEmptyBody(children: Children) -> impl IntoView {
41+
const OUTER_CLASS: &str = "animate-fade-in h-20 relative ";
42+
const INNER_CLASS: &str = "absolute inset-0 flex flex-col items-center \
43+
justify-center border-[2px] box-border \
44+
border-t-0 border-base-6 border-dashed rounded-b";
45+
4146
view! {
42-
<tr class="animate-fade-in h-20 relative border-[2px] border-t-0 border-base-6 border-dashed rounded-b">
47+
<tr class=OUTER_CLASS>
4348
<td></td><td></td><td></td><td></td>
44-
<div class="absolute inset-0 flex flex-col items-center justify-center">
49+
<div class=INNER_CLASS>
4550
{ children() }
4651
</div>
4752
</tr>

crates/site-app/src/hooks.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
//! Hooks are reusable pieces of reactive logic extracted from render contexts.
2+
13
mod cache_hook;
24
mod entry_hook;
5+
mod login_hook;
36
mod org_hook;
7+
mod signup_hook;
48

59
// pub use self::cache_hook::*;
6-
pub use self::{entry_hook::*, org_hook::*};
10+
pub use self::{entry_hook::*, login_hook::*, org_hook::*, signup_hook::*};
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use std::collections::HashMap;
2+
3+
use leptos::{ev::Event, prelude::*};
4+
use models::dvf::{EmailAddress, EmailAddressError};
5+
6+
use crate::{
7+
navigation::{navigate_to, next_url_hook},
8+
reactive_utils::touched_input_bindings,
9+
};
10+
11+
// #[derive(Clone, Copy)]
12+
pub struct LoginHook {
13+
email_signal: RwSignal<String>,
14+
password_signal: RwSignal<String>,
15+
submit_touched_signal: RwSignal<bool>,
16+
next_url_memo: Memo<String>,
17+
action: Action<(), Result<bool, String>>,
18+
}
19+
20+
impl LoginHook {
21+
pub fn new() -> Self {
22+
let email_signal = RwSignal::new(String::new());
23+
let password_signal = RwSignal::new(String::new());
24+
let submit_touched_signal = RwSignal::new(false);
25+
let next_url_memo = next_url_hook();
26+
27+
let action = Action::new_local(move |(): &()| {
28+
// json body for authenticate endpoint
29+
let body = HashMap::<_, String>::from_iter([
30+
("email", email_signal.get()),
31+
("password", password_signal.get()),
32+
]);
33+
async move {
34+
let resp = gloo_net::http::Request::post("/api/v1/authenticate")
35+
.json(&body)
36+
.expect("failed to build json authenticate payload")
37+
.send()
38+
.await
39+
.map_err(|e| format!("request error: {e}"))?;
40+
41+
match resp.status() {
42+
200 => Ok(true),
43+
401 => Ok(false),
44+
400 => Err(format!("response error: {}", resp.text().await.unwrap())),
45+
s => Err(format!("status error: got unknown status {s}")),
46+
}
47+
}
48+
});
49+
50+
Self {
51+
email_signal,
52+
password_signal,
53+
submit_touched_signal,
54+
next_url_memo,
55+
action,
56+
}
57+
}
58+
59+
pub fn email_bindings(&self) -> (impl Fn() -> String, impl Fn(Event)) {
60+
touched_input_bindings(self.email_signal)
61+
}
62+
63+
pub fn password_bindings(&self) -> (impl Fn() -> String, impl Fn(Event)) {
64+
touched_input_bindings(self.password_signal)
65+
}
66+
67+
pub fn email_error_hint(&self) -> impl Fn() -> Option<String> {
68+
let (email_signal, submit_touched_signal) =
69+
(self.email_signal, self.submit_touched_signal);
70+
move || {
71+
let email = email_signal.get();
72+
if !submit_touched_signal() {
73+
return None;
74+
}
75+
if email.is_empty() {
76+
return Some("Email address required.".into());
77+
}
78+
match EmailAddress::try_new(email) {
79+
Ok(_) => None,
80+
Err(EmailAddressError::LenCharMaxViolated) => {
81+
Some("That email address looks too long.".into())
82+
}
83+
Err(EmailAddressError::PredicateViolated) => {
84+
Some("That email address doesn't look right.".into())
85+
}
86+
}
87+
}
88+
}
89+
90+
pub fn password_error_hint(&self) -> impl Fn() -> Option<String> {
91+
let (password_signal, submit_touched_signal) =
92+
(self.password_signal, self.submit_touched_signal);
93+
move || {
94+
let password = password_signal.get();
95+
if !submit_touched_signal() {
96+
return None;
97+
}
98+
if password.is_empty() {
99+
return Some("Password required.".into());
100+
}
101+
None
102+
}
103+
}
104+
105+
pub fn show_spinner(&self) -> impl Fn() -> bool {
106+
let (pending, value) = (self.action.pending(), self.action.value());
107+
// show if the action is loading or completed successfully
108+
move || pending() || matches!(value.get(), Some(Ok(true)))
109+
}
110+
111+
pub fn button_text(&self) -> impl Fn() -> &'static str {
112+
let (pending, value) = (self.action.pending(), self.action.value());
113+
move || match (value.get(), pending()) {
114+
// if the action is loading at all
115+
(_, true) => "Loading...",
116+
// if it's completed successfully
117+
(Some(Ok(true)), _) => "Redirecting...",
118+
// any other state
119+
_ => "Log In",
120+
}
121+
}
122+
123+
pub fn action_trigger(&self) -> impl Fn() {
124+
let submit_touched_signal = self.submit_touched_signal;
125+
let email_error_hint = self.email_error_hint();
126+
let password_error_hint = self.password_error_hint();
127+
let action = self.action;
128+
129+
move || {
130+
submit_touched_signal.set(true);
131+
132+
if email_error_hint().is_some() || password_error_hint().is_some() {
133+
return;
134+
}
135+
136+
action.dispatch_local(());
137+
}
138+
}
139+
140+
pub fn create_redirect_effect(&self) -> Effect<LocalStorage> {
141+
let (action, next_url_memo) = (self.action, self.next_url_memo);
142+
Effect::new(move || {
143+
if action.value().get() == Some(Ok(true)) {
144+
navigate_to(&next_url_memo());
145+
}
146+
})
147+
}
148+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
use std::collections::HashMap;
2+
3+
use leptos::{ev::Event, prelude::*};
4+
use models::dvf::{EmailAddress, EmailAddressError, HumanName, HumanNameError};
5+
6+
use crate::{
7+
navigation::{navigate_to, next_url_hook},
8+
reactive_utils::touched_input_bindings,
9+
};
10+
11+
pub struct SignupHook {
12+
name_signal: RwSignal<String>,
13+
email_signal: RwSignal<String>,
14+
password_signal: RwSignal<String>,
15+
confirm_password_signal: RwSignal<String>,
16+
submit_touched_signal: RwSignal<bool>,
17+
next_url_memo: Memo<String>,
18+
action: Action<(), Result<bool, String>>,
19+
}
20+
21+
impl SignupHook {
22+
pub fn new() -> Self {
23+
let name_signal = RwSignal::new(String::new());
24+
let email_signal = RwSignal::new(String::new());
25+
let password_signal = RwSignal::new(String::new());
26+
let confirm_password_signal = RwSignal::new(String::new());
27+
let submit_touched_signal = RwSignal::new(false);
28+
let next_url_memo = next_url_hook();
29+
30+
let action = Action::new_local(move |(): &()| {
31+
// json body for authenticate endpoint
32+
let body = HashMap::<_, String>::from_iter([
33+
("name", name_signal.get()),
34+
("email", email_signal.get()),
35+
("password", password_signal.get()),
36+
]);
37+
async move {
38+
let resp = gloo_net::http::Request::post("/api/v1/signup")
39+
.json(&body)
40+
.expect("failed to build json authenticate payload")
41+
.send()
42+
.await
43+
.map_err(|e| format!("request error: {e}"))?;
44+
45+
match resp.status() {
46+
200 => Ok(true),
47+
401 => Ok(false),
48+
400 => Err(format!("response error: {}", resp.text().await.unwrap())),
49+
s => Err(format!("status error: got unknown status {s}")),
50+
}
51+
}
52+
});
53+
54+
Self {
55+
name_signal,
56+
email_signal,
57+
password_signal,
58+
confirm_password_signal,
59+
submit_touched_signal,
60+
next_url_memo,
61+
action,
62+
}
63+
}
64+
65+
pub fn name_bindings(&self) -> (impl Fn() -> String, impl Fn(Event)) {
66+
touched_input_bindings(self.name_signal)
67+
}
68+
69+
pub fn email_bindings(&self) -> (impl Fn() -> String, impl Fn(Event)) {
70+
touched_input_bindings(self.email_signal)
71+
}
72+
73+
pub fn password_bindings(&self) -> (impl Fn() -> String, impl Fn(Event)) {
74+
touched_input_bindings(self.password_signal)
75+
}
76+
77+
pub fn confirm_password_bindings(
78+
&self,
79+
) -> (impl Fn() -> String, impl Fn(Event)) {
80+
touched_input_bindings(self.confirm_password_signal)
81+
}
82+
83+
pub fn name_error_hint(&self) -> impl Fn() -> Option<String> {
84+
let (name_signal, submit_touched_signal) =
85+
(self.name_signal, self.submit_touched_signal);
86+
move || {
87+
let name = name_signal.get();
88+
if !submit_touched_signal() {
89+
return None;
90+
}
91+
if name.is_empty() {
92+
return Some("Your name is required.".into());
93+
}
94+
match HumanName::try_new(name) {
95+
Ok(_) => None,
96+
Err(HumanNameError::LenCharMaxViolated) => {
97+
Some("The name you entered is too long.".into())
98+
}
99+
Err(HumanNameError::NotEmptyViolated) => {
100+
Some("Your name is required.".into())
101+
}
102+
}
103+
}
104+
}
105+
106+
pub fn email_error_hint(&self) -> impl Fn() -> Option<String> {
107+
let (email_signal, submit_touched_signal) =
108+
(self.email_signal, self.submit_touched_signal);
109+
move || {
110+
let email = email_signal.get();
111+
if !submit_touched_signal() {
112+
return None;
113+
}
114+
if email.is_empty() {
115+
return Some("Email address required.".into());
116+
}
117+
match EmailAddress::try_new(email) {
118+
Ok(_) => None,
119+
Err(EmailAddressError::LenCharMaxViolated) => {
120+
Some("That email address looks too long.".into())
121+
}
122+
Err(EmailAddressError::PredicateViolated) => {
123+
Some("That email address doesn't look right.".into())
124+
}
125+
}
126+
}
127+
}
128+
129+
pub fn password_error_hint(&self) -> impl Fn() -> Option<String> {
130+
let (password_signal, submit_touched_signal) =
131+
(self.password_signal, self.submit_touched_signal);
132+
move || {
133+
let password = password_signal.get();
134+
if !submit_touched_signal() {
135+
return None;
136+
}
137+
if password.is_empty() {
138+
return Some("Password required.".into());
139+
}
140+
None
141+
}
142+
}
143+
144+
pub fn confirm_password_error_hint(&self) -> impl Fn() -> Option<String> {
145+
let (password_signal, confirm_password_signal, submit_touched_signal) = (
146+
self.password_signal,
147+
self.confirm_password_signal,
148+
self.submit_touched_signal,
149+
);
150+
move || {
151+
let password = password_signal.get();
152+
let confirm_password = confirm_password_signal.get();
153+
if !submit_touched_signal() {
154+
return None;
155+
}
156+
if confirm_password != password {
157+
return Some("Passwords don't match.".into());
158+
}
159+
None
160+
}
161+
}
162+
163+
pub fn show_spinner(&self) -> impl Fn() -> bool {
164+
let (pending, value) = (self.action.pending(), self.action.value());
165+
// show if the action is loading or completed successfully
166+
move || pending() || matches!(value.get(), Some(Ok(true)))
167+
}
168+
169+
pub fn button_text(&self) -> impl Fn() -> &'static str {
170+
let (pending, value) = (self.action.pending(), self.action.value());
171+
move || match (value.get(), pending()) {
172+
// if the action is loading at all
173+
(_, true) => "Loading...",
174+
// if it's completed successfully
175+
(Some(Ok(true)), _) => "Redirecting...",
176+
// any other state
177+
_ => "Sign Up",
178+
}
179+
}
180+
181+
pub fn action_trigger(&self) -> impl Fn() {
182+
let submit_touched_signal = self.submit_touched_signal;
183+
let name_error_hint = self.name_error_hint();
184+
let email_error_hint = self.email_error_hint();
185+
let password_error_hint = self.password_error_hint();
186+
let confirm_password_error_hint = self.confirm_password_error_hint();
187+
let action = self.action;
188+
189+
move || {
190+
submit_touched_signal.set(true);
191+
192+
if name_error_hint().is_some()
193+
|| email_error_hint().is_some()
194+
|| password_error_hint().is_some()
195+
|| confirm_password_error_hint().is_some()
196+
{
197+
return;
198+
}
199+
200+
action.dispatch_local(());
201+
}
202+
}
203+
204+
pub fn create_redirect_effect(&self) -> Effect<LocalStorage> {
205+
let (action, next_url_memo) = (self.action, self.next_url_memo);
206+
Effect::new(move || {
207+
if action.value().get() == Some(Ok(true)) {
208+
navigate_to(&next_url_memo());
209+
}
210+
})
211+
}
212+
}

0 commit comments

Comments
 (0)