diff --git a/frontend/src/components/form.rs b/frontend/src/components/form.rs index 7fab859..d7dddec 100644 --- a/frontend/src/components/form.rs +++ b/frontend/src/components/form.rs @@ -17,6 +17,14 @@ pub struct Form { content: String, fetch_task: Option, loading: bool, + validation_errors: ValidationErrors, +} + +#[derive(Default)] +struct ValidationErrors { + name: Option, + email: Option, + content: Option, } pub enum Msg { @@ -39,6 +47,7 @@ impl Component for Form { content: String::new(), fetch_task: None, loading: false, + validation_errors: ValidationErrors::default(), } } @@ -46,50 +55,64 @@ impl Component for Form { match msg { Msg::UpdateName(name) => { self.name = name; + self.validate_name(); true } Msg::UpdateEmail(email) => { self.email = email; + self.validate_email(); true } Msg::UpdateContent(content) => { self.content = content; + self.validate_content(); true } Msg::Submit => { - self.loading = true; - - let form_data = FormData { - name: self.name.clone(), - email: self.email.clone(), - content: self.content.clone(), - }; - - let request = Request::post("https://ask.scg.skku.ac.kr/v1/send") - .header("Content-Type", "application/json; charset=utf-8") - .body(Json(&form_data)) - .expect("Could not build request."); - - let callback = self.link.callback(|response: Response| { - if response.status().is_success() { - Msg::Response(Ok(())) - } else { - Msg::Response(Err(anyhow::anyhow!("Request failed"))) - } - }); + self.validate_all(); + + if self.is_valid() && !self.loading { + self.loading = true; - let task = FetchService::fetch(request, callback).expect("Failed to start request"); - self.fetch_task = Some(task); + let form_data = FormData { + name: self.name.clone(), + email: self.email.clone(), + content: self.content.clone(), + }; + + let request = Request::post("https://ask.scg.skku.ac.kr/v1/send") + .header("Content-Type", "application/json; charset=utf-8") + .body(Json(&form_data)) + .expect("Could not build request."); + + let callback = self.link.callback(|response: Response| { + if response.status().is_success() { + Msg::Response(Ok(())) + } else { + Msg::Response(Err(anyhow::anyhow!("Request failed"))) + } + }); + + let task = + FetchService::fetch(request, callback).expect("Failed to start request"); + self.fetch_task = Some(task); + } - false + true } Msg::Response(result) => { self.loading = false; + self.fetch_task = None; match result { - Ok(_) => log::info!("Request successful"), + Ok(_) => { + log::info!("Request successful"); + self.name = String::new(); + self.email = String::new(); + self.content = String::new(); + self.validation_errors = ValidationErrors::default(); + } Err(err) => log::error!("Request failed: {:?}", err), } - self.fetch_task = None; true } } @@ -100,6 +123,8 @@ impl Component for Form { } fn view(&self) -> Html { + let is_disabled = self.loading || !self.is_valid(); + html! {
+ { + if let Some(error) = &self.validation_errors.name { + html! {

{error}

} + } else { + html! {} + } + }
@@ -124,6 +156,13 @@ impl Component for Form { value=self.email.clone() oninput=self.link.callback(|e: InputData| Msg::UpdateEmail(e.value)) /> + { + if let Some(error) = &self.validation_errors.email { + html! {

{error}

} + } else { + html! {} + } + }
@@ -133,9 +172,68 @@ impl Component for Form { value={self.content.clone()} oninput=self.link.callback(|e: InputData| Msg::UpdateContent(e.value)) /> + { + if let Some(error) = &self.validation_errors.content { + html! {

{error}

} + } else { + html! {} + } + }
- +
} } } + +impl Form { + fn validate_name(&mut self) { + if self.name.trim().is_empty() { + self.validation_errors.name = Some("이름을 작성해주세요.".into()); + } else { + self.validation_errors.name = None; + } + } + + fn validate_email(&mut self) { + if self.email.trim().is_empty() { + self.validation_errors.email = Some("이메일을 작성해주세요.".into()); + } else if !self.email.contains("@") { + self.validation_errors.email = Some("올바른 이메일 주소를 입력해주세요.".into()); + } else { + self.validation_errors.email = None; + } + } + + fn validate_content(&mut self) { + if self.content.trim().is_empty() { + self.validation_errors.content = Some("내용을 작성해주세요.".into()); + } else { + self.validation_errors.content = None; + } + } + + fn validate_all(&mut self) { + self.validate_name(); + self.validate_email(); + self.validate_content(); + } + + fn is_valid(&self) -> bool { + self.validation_errors.name.is_none() + && self.validation_errors.email.is_none() + && self.validation_errors.content.is_none() + } +} diff --git a/frontend/static/styles.css b/frontend/static/styles.css index edd3893..3103a7a 100644 --- a/frontend/static/styles.css +++ b/frontend/static/styles.css @@ -182,6 +182,9 @@ button[type="submit"]:disabled { background-color: #9f9f9f; cursor: not-allowed; } +button[type="submit"]:disabled:hover { + background-color: #9f9f9f; +} button[type="submit"]:hover { background-color: #dddddd; @@ -253,4 +256,20 @@ button[type="submit"]:active { .loading_spinner_text { margin-top: 10px; +} + +.spinner { + border: 2px solid #f3f3f3; + border-top: 2px solid #3498db; + border-radius: 50%; + width: 16px; + height: 16px; + animation: spin 2s linear infinite; + display: inline-block; + margin-right: 8px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } \ No newline at end of file