diff --git a/.gitignore b/.gitignore index 50c8301..3217bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ Cargo.lock # These are backup files generated by rustfmt -**/*.rs.bk \ No newline at end of file +**/*.rs.bk +**/dist/* \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9670d21..5ef2a69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/wiseaidev/input-yew" documentation = "https://docs.rs/input_yew/" authors = ["Mahmoud Harmouch "] edition = "2021" -exclude = ["/assets"] +exclude = ["/assets", "/examples"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/LICENSE b/LICENSE index 137ac81..8f1b239 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Mahmoud Harmouch + Copyright 2024 Mahmoud Harmouch Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 650c31c..bbf601f 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,16 @@ Using this custom reusable input component is a breeze! Simply follow these step html! {
| Indicates whether the input is required or not. | true, false. | None | +| input_type | &'static str | The type of the input. | "text", "password", "tel, "textarea", "date". | "text" | +| label | &'static str | The label to be displayed for the input field. | "Username", "Email". | "" | +| name | &'static str | The name of the input field, used for form submission and accessibility. | "username", "email". | "" | +| required | bool | Indicates whether the input is required or not. | true, false. | false | | input_ref | NodeRef | A reference to the DOM node of the input element. | `use_node_ref()`, | - | -| error_message | AttrValue | The error message to display when there is a validation error. | "Invalid input", "Field is required". | AttrValue::default() | -| form_input_class | AttrValue | The CSS class to be applied to all inner elements. | "form-input-container", "input-group". | AttrValue::default() | -| form_input_field_class | AttrValue | The CSS class to be applied to the inner input element and icon. | "form-input-field", "input-icon". | AttrValue::default() | -| form_input_label_class | AttrValue | The CSS class to be applied to the label for the input element. | "form-input-label". | AttrValue::default() | -| form_input_input_class | AttrValue | The CSS class to be applied to the input element. | "custom-input". | AttrValue::default() | -| form_input_error_class | AttrValue | The CSS class to be applied to the error div element. | "input-error-message". | AttrValue::default() | -| icon_class | AttrValue | The CSS class to be applied to the start icon element. | "input-icon". | AttrValue::default() | -| input_handle | UseStateHandle | The state handle for managing the value of the input. | use_state("initial value".into()).handle(), | - | -| input_valid_handle | UseStateHandle | The state handle for managing the validity state of the input. | use_state(true).handle(), | - | +| error_message | &'static str | The error message to display when there is a validation error. | "Invalid input", "Field is required". | "" | + +### Styling Properties + +| Name | Type | Description | Example | Default Value | +| --- | --- | --- | --- | --- | +| form_input_class | &'static str | The CSS class to be applied to all inner elements. | "form-input-container", "input-group". | "" | +| form_input_field_class | &'static str | The CSS class to be applied to the inner input element and icon. | "form-input-field", "input-icon". | "" | +| form_input_label_class | &'static str | The CSS class to be applied to the label for the input element. | "form-input-label". | "" | +| form_input_input_class | &'static str | The CSS class to be applied to the input element. | "custom-input". | "" | +| form_input_error_class | &'static str | The CSS class to be applied to the error div element. | "input-error-message". | "" | +| icon_class | &'static str | The CSS class to be applied to the start icon element. | "input-icon". | "" | + +### State and Callback Properties + +| Name | Type | Description | Example | Default Value | +| --- | --- | --- | --- | --- | +| input_handle | UseStateHandle | The state handle for managing the value of the input. | use_state(|| "initial value".to_string()), | - | +| input_valid_handle | UseStateHandle | The state handle for managing the validity state of the input. | use_state(|| true), | - | | validate_function | Callback | A callback function to validate the input value. It takes a `String` as input and returns a `bool`. | Callback::from(|value: String| value.len() >= 8), | - | -| eye_active | AttrValue | The icon when the password is visible. | "fa fa-eye" in case of using **FontAwesome**. | AttrValue::from("fa fa-eye") | -| eye_disabled | AttrValue | The icon when the password is not visible. | "fa fa-eye-slash" in case of using **FontAwesome**. | AttrValue::from("fa fa-eye-slash") | -| input_id | AttrValue | The ID attribute of the input element. | "input-username", "input-email". | AttrValue::default() | -| input_placeholder | AttrValue | The placeholder text to be displayed in the input element. | "Enter your username", "Type your email". | AttrValue::default() | -| aria_label | AttrValue | The aria-label attribute for screen readers, providing a label for accessibility. | "Username input", "Email input". | AttrValue::default() | -| aria_required | AttrValue | The aria-required attribute for screen readers, indicating whether the input is required. | "true", "false". | AttrValue::from("true") | -| aria_invalid | AttrValue | The aria-invalid attribute for screen readers, indicating whether the input value is invalid. | "true", "false". | AttrValue::from("true") | -| aria_describedby | AttrValue | The aria-describedby attribute for screen readers, describing the input element's error message. | "error-message-username", "error-message-email". | AttrValue::default() | -## 📙 Examples +### Icon Properties + +| Name | Type | Description | Example | Default Value | +| --- | --- | --- | --- | --- | +| eye_active | &'static str | The icon when the password is visible. | "fa fa-eye" in case of using **FontAwesome**. | "fa fa-eye" | +| eye_disabled | &'static str | The icon when the password is not visible. | "fa fa-eye-slash" in case of using **FontAwesome**. | "fa fa-eye-slash" | + +### Accessibility and SEO Properties -Lots of repositories we built use it to create even more sophisticated forms like Contact Us forms, multi-step forms, and login forms. If you're curious about how to use it, you can check out the following repositories for more information: +| Name | Type | Description | Example | Default Value | +| --- | --- | --- | --- | --- | +| input_id | &'static str | The ID attribute of the input element. | "input-username", "input-email". | "" | +| input_placeholder | &'static str | The placeholder text to be displayed in the input element. | "Enter your username", "Type your email". | "" | +| aria_label | &'static str | The aria-label attribute for screen readers, providing a label for accessibility. | "Username input", "Email input". | "" | +| aria_required | &'static str | The aria-required attribute for screen readers, indicating whether the input is required. | "true", "false". | "true" | +| aria_invalid | &'static str | The aria-invalid attribute for screen readers, indicating whether the input value is invalid. | "true", "false". | "true" | +| aria_describedby | &'static str | The aria-describedby attribute for screen readers, describing the input element's error message. | "error-message-username", "error-message-email". | "" | + +## 📙 Examples -- [Yew Tailwind Components](https://github.com/wiseaidev/yew-components-tailwind). -- [Yew Pure CSS Components](https://github.com/wiseaidev/yew-components-pure-css). +Lots of examples we built use it to create even more sophisticated forms like Contact Us forms, multi-step forms, and login forms. If you're curious about how to use it, you can check out [the examples folder](examples) for more information. ## 🤝 Contribution -We welcome contributions from the community to make the Input Yew even better! Feel free to open issues, submit pull requests, or provide feedback. Let's collaborate and create something amazing together! +We welcome contributions from the community to make this input component even better! Feel free to open issues, submit pull requests, or provide feedback. Let's collaborate and create something amazing together! ## 📜 License -The Yew Custom Reusable Input Component is licensed under the `Apache-2.0` License, giving you the freedom to use, modify, and distribute it as you see fit. Please check the `LICENSE` file for more details. +This Yew component is licensed under the `Apache-2.0` License, giving you the freedom to use, modify, and distribute it as you see fit. Please check the [`LICENSE`](LICENSE) file for more details. -## 📝 Epilogue +## 📝 Conclusion -Congratulations! You're now equipped with a fantastic Yew Custom Reusable Input Component that will supercharge your web applications with its flexibility, user-friendliness, and robustness. Happy coding, and may your projects thrive with this powerful tool! 🎉 +Congratulations! You're now equipped with a Custom Reusable Input Component that will supercharge your Yew applications with its flexibility, user-friendliness, and robustness. Happy coding, and may your projects thrive with this powerful tool! 🎉 diff --git a/examples/tailwind/.gitignore b/examples/tailwind/.gitignore new file mode 100644 index 0000000..e512471 --- /dev/null +++ b/examples/tailwind/.gitignore @@ -0,0 +1 @@ +target/**/* \ No newline at end of file diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml new file mode 100644 index 0000000..8103c5a --- /dev/null +++ b/examples/tailwind/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "yew-components-tailwind" +version = "0.1.1" +authors = ["Mahmoud Harmouch "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +input_yew = { path = "../.." } +regex = { version = "1.9.1", default-features = false } +reqwasm = { version = "0.5.0", default-features = false } +serde = { version = "1.0.178", default-features = false } +serde_json = { version = "1.0.104", default-features = false } +wasm-bindgen = { version = "0.2.87", default-features = false } +wasm-bindgen-futures = { version = "0.4.37", default-features = false } +web-sys = { version = "0.3.64", default-features = false } +yew = { version = "0.21.0", features = ["csr"], default-features = false } +yew-router = { version = "0.18.0", default-features = false } + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = "thin" +strip = "symbols" diff --git a/examples/tailwind/LICENSE b/examples/tailwind/LICENSE new file mode 100644 index 0000000..137ac81 --- /dev/null +++ b/examples/tailwind/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Mahmoud Harmouch + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md new file mode 100644 index 0000000..bde5939 --- /dev/null +++ b/examples/tailwind/README.md @@ -0,0 +1,52 @@ +# 📚 Yew Tailwind Components + +## 📖 Introduction + +[Yew](https://yew.rs/) is a modern Rust framework for building multi-threaded front-end web applications. It aims to provide a productive and pleasant experience for developing front-end applications in Rust, leveraging its safety and performance benefits. By utilizing Yew, we can create interactive and efficient web applications with ease. + +## 🚀 Building and Running + +1. Fork/Clone the GitHub repository. + + ```bash + git clone https://github.com/wiseaidev/input-yew + ``` + +1. Navigate to the application directory. + + ```bash + cd examples/tailwind + ``` + +1. Run the client: + + ```sh + trunk serve --port 3000 + ``` + +Navigate to http://localhost:3000 to explore all available components. + +## 🌀 Tailwind CSS Components + +This section lists components implemented using the [Tailwind CSS](https://tailwindcss.com/) framework. Tailwind CSS is a utility-first CSS framework that provides a set of pre-defined classes to quickly build custom and responsive designs. + +### 🔐 Login Forms + +| ID | Preview | Demo | Localhost | +|---|---|---|---| +| 1 | ![Component 1](./assets/form-one.png) | [![Netlify Status](https://api.netlify.com/api/v1/badges/68d1469e-05ee-4acd-9368-b67d9e53bc2e/deploy-status)](https://tailwind-login-form-1.netlify.app/) | [Localhost](http://localhost:3000/login/1) | +| 2 | ![Component 2](./assets/form-two.png) | [![Netlify Status](https://api.netlify.com/api/v1/badges/68d1469e-05ee-4acd-9368-b67d9e53bc2e/deploy-status)](https://tailwind-login-form-2.netlify.app/) | [Localhost](http://localhost:3000/login/2) | +| 3 | ![Component 3](./assets/form-three.png) | [![Netlify Status](https://api.netlify.com/api/v1/badges/68d1469e-05ee-4acd-9368-b67d9e53bc2e/deploy-status)](https://tailwind-login-form-3.netlify.app/) | [Localhost](http://localhost:3000/login/3) | + +### 📬 Contact Forms + +| ID | Preview | Demo | Localhost | +|---|---|---|---| +| 1 | ![Component 1](./assets/contact-form-one.png) | [![Netlify Status](https://api.netlify.com/api/v1/badges/68d1469e-05ee-4acd-9368-b67d9e53bc2e/deploy-status)](https://tailwind-contact-form-1.netlify.app/) | [Localhost](http://localhost:3000/contact/1) | + +### 🔢 Multi-Steps Forms + +| ID | Preview | Demo | Localhost | +|---|---|---|---| +| 1 | ![Component 1](./assets/multi-step-form-one.png) | [![Netlify Status](https://api.netlify.com/api/v1/badges/68d1469e-05ee-4acd-9368-b67d9e53bc2e/deploy-status)](https://tailwind-multi-step-form-1.netlify.app/) | [Localhost](http://localhost:3000/multi-step/1) | + diff --git a/examples/tailwind/Trunk.toml b/examples/tailwind/Trunk.toml new file mode 100644 index 0000000..123d93b --- /dev/null +++ b/examples/tailwind/Trunk.toml @@ -0,0 +1,4 @@ +[[hooks]] +stage = "post_build" +command = "sh" +command_arguments = ["-c", "npx tailwindcss -i ./css/tailwind.css -o ./dist/.stage/index.css"] \ No newline at end of file diff --git a/examples/tailwind/_redirects b/examples/tailwind/_redirects new file mode 100644 index 0000000..f824337 --- /dev/null +++ b/examples/tailwind/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/examples/tailwind/assets/contact-form-one.png b/examples/tailwind/assets/contact-form-one.png new file mode 100644 index 0000000..ae23cdf Binary files /dev/null and b/examples/tailwind/assets/contact-form-one.png differ diff --git a/examples/tailwind/assets/form-one.png b/examples/tailwind/assets/form-one.png new file mode 100644 index 0000000..38988f0 Binary files /dev/null and b/examples/tailwind/assets/form-one.png differ diff --git a/examples/tailwind/assets/form-three.png b/examples/tailwind/assets/form-three.png new file mode 100644 index 0000000..19c70b7 Binary files /dev/null and b/examples/tailwind/assets/form-three.png differ diff --git a/examples/tailwind/assets/form-two.png b/examples/tailwind/assets/form-two.png new file mode 100644 index 0000000..1cb1985 Binary files /dev/null and b/examples/tailwind/assets/form-two.png differ diff --git a/examples/tailwind/assets/multi-step-form-one.png b/examples/tailwind/assets/multi-step-form-one.png new file mode 100644 index 0000000..e4d20c0 Binary files /dev/null and b/examples/tailwind/assets/multi-step-form-one.png differ diff --git a/examples/tailwind/css/tailwind.css b/examples/tailwind/css/tailwind.css new file mode 100644 index 0000000..a09a48a --- /dev/null +++ b/examples/tailwind/css/tailwind.css @@ -0,0 +1,12 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; +@tailwind variants; + +.telephone-input select { + max-width: 55px; + font-size: 14px; + padding: 10px; +} \ No newline at end of file diff --git a/examples/tailwind/index.html b/examples/tailwind/index.html new file mode 100644 index 0000000..ffbac1d --- /dev/null +++ b/examples/tailwind/index.html @@ -0,0 +1,14 @@ + + + + + + + Yew Tailwind Forms + + + diff --git a/examples/tailwind/postcss.config.js b/examples/tailwind/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/examples/tailwind/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/tailwind/src/api/auth.rs b/examples/tailwind/src/api/auth.rs new file mode 100644 index 0000000..3fb2c2c --- /dev/null +++ b/examples/tailwind/src/api/auth.rs @@ -0,0 +1,53 @@ +use reqwasm::http::{Request, RequestCredentials}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +const BASE_URL: &str = "http://localhost:8080/api/v1"; + +#[derive(Serialize, Deserialize, Debug)] +pub struct UserLoginResponse { + pub status: String, + pub token: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ErrorResponse { + pub status: String, + pub message: String, +} + +pub async fn login_user(username: String, password: String) -> Result { + let response = match Request::post(&format!("{}/auth/login", BASE_URL)) + .header("Content-Type", "application/json") + .credentials(RequestCredentials::Include) + .body( + json!({ + "username": username, + "password": password + }) + .to_string(), + ) + .send() + .await + { + Ok(res) => res, + Err(_) => { + return Err("Network Error!".to_string()); + } + }; + + if response.status() != 200 { + let error_response = response.json::().await; + if let Ok(error_response) = error_response { + return Err(error_response.message); + } else { + return Err(format!("Network Error: {}", response.status())); + } + } + + let res_json = response.json::().await; + match res_json { + Ok(data) => Ok(data), + Err(_) => Err("Failed to parse response".to_string()), + } +} diff --git a/examples/tailwind/src/api/mod.rs b/examples/tailwind/src/api/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/examples/tailwind/src/api/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/examples/tailwind/src/app.rs b/examples/tailwind/src/app.rs new file mode 100644 index 0000000..0fc22ea --- /dev/null +++ b/examples/tailwind/src/app.rs @@ -0,0 +1,13 @@ +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::router::{switch, Route}; + +#[function_component(App)] +pub fn app() -> Html { + html! { + + render={switch} /> + + } +} diff --git a/examples/tailwind/src/components/common.rs b/examples/tailwind/src/components/common.rs new file mode 100644 index 0000000..c4edb98 --- /dev/null +++ b/examples/tailwind/src/components/common.rs @@ -0,0 +1,17 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct LoginUserSchema { + email: String, + password: String, +} + +pub fn validate_email(email: String) -> bool { + let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap(); + pattern.is_match(&email) +} + +pub fn validate_input(field: String) -> bool { + !&field.is_empty() +} diff --git a/examples/tailwind/src/components/contact_form_one.rs b/examples/tailwind/src/components/contact_form_one.rs new file mode 100644 index 0000000..56cfbcc --- /dev/null +++ b/examples/tailwind/src/components/contact_form_one.rs @@ -0,0 +1,185 @@ +use crate::components::common::{validate_email, validate_input, LoginUserSchema}; +use input_yew::CustomInput; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_futures::spawn_local; +use web_sys::{console, HtmlInputElement, Window}; +use yew::prelude::*; + +use crate::api::auth::login_user; + +#[function_component(ContactFormOne)] +pub fn contact_form_one() -> Html { + let error_handle = use_state(String::default); + let error = (*error_handle).clone(); + + let email_valid_handle = use_state(|| true); + let email_valid = (*email_valid_handle).clone(); + + let name_valid_handle = use_state(|| true); + let name_valid = (*name_valid_handle).clone(); + + let subject_valid_handle = use_state(|| true); + let subject_valid = (*subject_valid_handle).clone(); + + let message_valid_handle = use_state(|| true); + let message_valid = (*message_valid_handle).clone(); + + let input_email_ref = use_node_ref(); + let input_email_handle = use_state(String::default); + let input_email = (*input_email_handle).clone(); + + let input_name_ref = use_node_ref(); + let input_name_handle = use_state(|| "afasfasf".to_string()); + let input_name = (*input_name_handle).clone(); + + let input_subject_ref = use_node_ref(); + let input_subject_handle = use_state(String::default); + let input_subject = (*input_subject_handle).clone(); + + let input_message_ref = use_node_ref(); + let input_message_handle = use_state(String::default); + let input_message = (*input_message_handle).clone(); + + let onsubmit = Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + + let email_ref = input_email.clone(); + let name_ref = input_name.clone(); + let subject_ref = input_subject.clone(); + let message_ref = input_message.clone(); + let error_handle = error_handle.clone(); + console::log_1( + &format!( + "Email: {}, Name: {}, Subject: {}, Message: {}", + input_email, input_name, input_subject, input_message + ) + .into(), + ); + + spawn_local(async move { + let email_val = email_ref.clone(); + let _name_val = name_ref.clone(); + let subject_val = subject_ref.clone(); + let _message_val = message_ref.clone(); + + let error_handle = error_handle.clone(); + if email_valid && name_valid && subject_valid && message_valid { + // TODO: create a contact us endpoint + let response = login_user(email_val.to_string(), subject_val.to_string()).await; + match response { + Ok(_) => { + console::log_1(&"success".into()); + let window: Window = web_sys::window().expect("window not available"); + let location = window.location(); + let _ = location.set_href("/home"); + } + Err(err) => { + error_handle.set(err); + } + } + } else { + error_handle.set("Please provide valid contact information!".into()); + } + }); + }); + + html! { +
+
+
+ + + if !error.is_empty() { +
{error}
+ } + + {"Contact US"} + + + + + +
+ +
+ +
+
+
+ } +} diff --git a/examples/tailwind/src/components/count.rs b/examples/tailwind/src/components/count.rs new file mode 100644 index 0000000..22d4a60 --- /dev/null +++ b/examples/tailwind/src/components/count.rs @@ -0,0 +1,289 @@ +use std::time::{Duration, Instant}; +use yew::prelude::*; + +#[derive(Clone, Properties)] +pub struct CountUpOptions { + pub start_val: f64, + pub decimal_places: usize, + pub duration: f64, + pub use_grouping: bool, + pub use_indian_separators: bool, + pub use_easing: bool, + pub smart_easing_threshold: f64, + pub smart_easing_amount: f64, + pub separator: String, + pub decimal: String, + pub prefix: String, + pub suffix: String, + pub enable_scroll_spy: bool, + pub scroll_spy_delay: u64, + pub scroll_spy_once: bool, + pub on_complete_callback: Callback<()>, + pub on_start_callback: Callback<()>, +} + +pub fn count_up(options: CountUpOptions) -> Html { + let start_val = options.start_val; + let duration = options.duration * 1000.0; // Convert seconds to milliseconds + + let (link, state) = use_state(|| CountUpState { + options, + start_val, + end_val: 0.0, + duration, + frame_val: start_val, + paused: true, + final_end_val: None, + use_easing: true, + count_down: false, + error: None, + start_time: None, + remaining: duration, + }); + + let on_start = link.callback(|_| Msg::Start); + let on_pause_resume = link.callback(|_| Msg::PauseResume); + let on_reset = link.callback(|_| Msg::Reset); + let on_update = link.callback(|new_end_val| Msg::Update(new_end_val)); + + html! { +
+ { state.frame_val } + + + + +
+ } +} +#[derive(Clone, Properties)] +pub struct CountUpOptions { + pub start_val: f64, + pub decimal_places: usize, + pub duration: f64, + pub use_grouping: bool, + pub use_indian_separators: bool, + pub use_easing: bool, + pub smart_easing_threshold: f64, + pub smart_easing_amount: f64, + pub separator: String, + pub decimal: String, + pub prefix: String, + pub suffix: String, + pub enable_scroll_spy: bool, + pub scroll_spy_delay: u64, + pub scroll_spy_once: bool, + pub on_complete_callback: Callback<()>, + pub on_start_callback: Callback<()>, +} +struct CountUpState { + options: CountUpOptions, + start_val: f64, + end_val: f64, + duration: f64, + frame_val: f64, + paused: bool, + final_end_val: Option, + use_easing: bool, + count_down: bool, + error: Option, + start_time: Option, + remaining: f64, +} + +enum Msg { + Start, + PauseResume, + Reset, + Update(f64), + Tick(Instant), +} + +impl Component for CountUpState { + type Message = Msg; + type Properties = CountUpOptions; + + fn create(props: Self::Properties, _link: ComponentLink) -> Self { + let start_val = props.start_val; + let duration = props.duration * 1000.0; // Convert seconds to milliseconds + let remaining = duration; + CountUpState { + options: props, + start_val, + end_val: 0.0, + duration, + frame_val: start_val, + paused: true, + final_end_val: None, + use_easing: true, + count_down: false, + error: None, + start_time: None, + remaining, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::Start => { + if let Some(on_start_callback) = &self.options.on_start_callback { + on_start_callback.emit(()); + } + if self.duration > 0.0 { + self.determine_direction_and_smart_easing(); + self.paused = false; + self.start_time = Some(Instant::now()); + self.link.send_message(Msg::Tick(self.start_time.unwrap())); + } else { + self.print_value(self.end_val); + } + } + Msg::PauseResume => { + if !self.paused { + self.paused = true; + } else { + self.start_time = None; + self.duration = self.remaining; + self.start_val = self.frame_val; + self.determine_direction_and_smart_easing(); + self.link.send_message(Msg::Tick(Instant::now())); + } + } + Msg::Reset => { + self.paused = true; + self.reset_duration(); + self.start_val = self.options.start_val; + self.frame_val = self.start_val; + self.print_value(self.start_val); + } + Msg::Update(new_end_val) => { + self.end_val = new_end_val; + if self.end_val == self.frame_val { + return false; + } + self.start_val = self.frame_val; + if self.final_end_val.is_none() { + self.reset_duration(); + } + self.final_end_val = None; + self.determine_direction_and_smart_easing(); + self.link.send_message(Msg::Tick(Instant::now())); + } + Msg::Tick(timestamp) => { + if self.start_time.is_none() { + self.start_time = Some(timestamp); + } + + let progress = timestamp.duration_since(self.start_time.unwrap()).as_millis() as f64; + self.remaining = self.duration - progress; + + // To ease or not to ease + if self.use_easing { + if self.count_down { + self.frame_val = self.start_val + - self.easing_fn(progress, 0.0, self.start_val - self.end_val, self.duration); + } else { + self.frame_val = self.easing_fn(progress, self.start_val, self.end_val - self.start_val, self.duration); + } + } else { + self.frame_val = self.start_val + (self.end_val - self.start_val) * (progress / self.duration); + } + + // Don't go past end_val since progress can exceed duration in the last frame + let went_past = if self.count_down { self.frame_val < self.end_val } else { self.frame_val > self.end_val }; + if went_past { + self.frame_val = self.end_val; + } + + // Decimal + self.frame_val = (self.frame_val * 10_f64.powi(self.options.decimal_places as i32)).round() + / 10_f64.powi(self.options.decimal_places as i32); + + // Format and print value + self.print_value(self.frame_val); + + // Whether to continue + if progress < self.duration { + self.link.send_message(Msg::Tick(timestamp)); + } else if self.final_end_val.is_some() { + // Smart easing + self.link.send_message(Msg::Update(self.final_end_val.unwrap())); + } else { + if let Some(on_complete_callback) = &self.options.on_complete_callback { + on_complete_callback.emit(()); + } + } + } + } + true + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.options = props; + self.start_val = self.options.start_val; + self.end_val = self.options.end_val; + self.reset_duration(); + self.final_end_val = None; + self.paused = true; + self.frame_val = self.start_val; + true + } + + fn view(&self) -> Html { + html! { +
+ { self.frame_val } + + + + +
+ } + } +} + +impl CountUpState { + fn determine_direction_and_smart_easing(&mut self) { + let end = self.final_end_val.unwrap_or(self.end_val); + self.count_down = self.start_val > end; + let animate_amount = end - self.start_val; + if animate_amount.abs() > self.options.smart_easing_threshold && self.options.use_easing { + self.final_end_val = Some(end); + let up = if self.count_down { 1.0 } else { -1.0 }; + self.end_val = end + (up * self.options.smart_easing_amount); + self.duration /= 2.0; + } else { + self.end_val = end; + self.final_end_val = None; + } + if self.final_end_val.is_none() { + self.use_easing = self.options.use_easing; + } else { + // Setting final_end_val indicates smart easing + self.use_easing = false; + } + } + + fn reset_duration(&mut self) { + self.start_time = None; + self.duration = self.options.duration * 1000.0; // Convert seconds to milliseconds + self.remaining = self.duration; + } + + fn print_value(&self, val: f64) { + // Implement rendering logic here + // You can use self.options to access formatting options + // and update the rendered value in your HTML elements. + } + + // Add your easing function here + fn easing_fn(&self, t: f64, b: f64, c: f64, d: f64) -> f64 { + // Implement your easing function here + // Example: easeOutExpo + c * (-2.0_f64.powf(-10.0 * t / d) + 1.0) + b + } +} + +fn main() { + yew::start_app::(); +} \ No newline at end of file diff --git a/examples/tailwind/src/components/login_form_one.rs b/examples/tailwind/src/components/login_form_one.rs new file mode 100644 index 0000000..357e357 --- /dev/null +++ b/examples/tailwind/src/components/login_form_one.rs @@ -0,0 +1,155 @@ +use crate::components::common::{validate_email, validate_input, LoginUserSchema}; +use input_yew::CustomInput; +use regex::Regex; +use wasm_bindgen_futures::spawn_local; +use web_sys::{console, HtmlInputElement, Window}; +use yew::prelude::*; + +use crate::api::auth::login_user; + +#[function_component(LoginFormOne)] +pub fn login_form_one() -> Html { + let error_handle = use_state(String::default); + let error = (*error_handle).clone(); + + let email_valid_handle = use_state(|| true); + let email_valid = (*email_valid_handle).clone(); + + let password_valid_handle = use_state(|| true); + let password_valid = (*password_valid_handle).clone(); + + let input_email_ref = use_node_ref(); + let input_email_handle = use_state(|| "sad".to_string()); + let input_email = (*input_email_handle).clone(); + + let input_password_ref = use_node_ref(); + let input_password_handle = use_state(|| "sad".to_string()); + let input_password = (*input_password_handle).clone(); + + let onsubmit = Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + + let email_ref = input_password.clone(); + let password_ref = input_password.clone(); + let error_handle = error_handle.clone(); + console::log_1(&format!("Email: {}, Password: {}", input_email, input_password).into()); + + spawn_local(async move { + let email_val = email_ref.clone(); + let password_val = password_ref.clone(); + let error_handle = error_handle.clone(); + if email_valid && password_valid { + let response = login_user(email_val.to_string(), password_val.to_string()).await; + match response { + Ok(_) => { + console::log_1(&"success".into()); + let window: Window = web_sys::window().expect("window not available"); + let location = window.location(); + let _ = location.set_href("/error"); + } + Err(err) => { + error_handle.set(err); + } + } + } else { + error_handle.set("Please provide a valid email and password!".into()); + } + }); + }); + + html! { +
+
+
+ if !error.is_empty() { +
{error}
+ } + {"Login"} + + +
+ +
+
+ {"Not a member?"} + {"Sign Up Now"} +
+
+
+ {"Or Sign In With"} +
+
+
+
+
+ + + + +
+
+ +
+
+ } +} diff --git a/examples/tailwind/src/components/login_form_three.rs b/examples/tailwind/src/components/login_form_three.rs new file mode 100644 index 0000000..fd3952f --- /dev/null +++ b/examples/tailwind/src/components/login_form_three.rs @@ -0,0 +1,273 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_futures::spawn_local; +use web_sys::{console, HtmlInputElement, Window}; +use yew::prelude::*; + +use crate::api::auth::login_user; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct LoginUserSchema { + email: String, + password: String, +} + +#[function_component(LoginFormThree)] +pub fn login_form_three() -> Html { + let error_handle = use_state(String::default); + let error = (*error_handle).clone(); + + let email_valid_handle = use_state(|| true); + let email_valid = (*email_valid_handle).clone(); + + let eye_active_handle = use_state(|| false); + let eye_active = (*eye_active_handle).clone(); + + let password_valid_handle = use_state(|| true); + let password_valid = (*password_valid_handle).clone(); + + let password_type_handle = use_state(|| "text"); + let password_type = (*password_type_handle).clone(); + + let input_email_ref = use_node_ref(); + let input_email_handle = use_state(String::default); + let input_email = (*input_email_handle).clone(); + + let input_password_ref = use_node_ref(); + let input_password_handle = use_state(String::default); + let input_password = (*input_password_handle).clone(); + + let validate_email = |email: &str| { + let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap(); + pattern.is_match(email) + }; + + let validate_password = |password: &str| !password.is_empty(); + + let on_email_change = { + let input_email_ref = input_email_ref.clone(); + + Callback::from(move |_| { + let input = input_email_ref.cast::(); + + if let Some(input) = input { + let value = input.value(); + input_email_handle.set(value); + email_valid_handle.set(validate_email(&input.value())); + } + }) + }; + + let on_password_change = { + let input_password_ref = input_password_ref.clone(); + + Callback::from(move |_| { + let input = input_password_ref.cast::(); + + if let Some(input) = input { + let value = input.value(); + input_password_handle.set(value); + password_valid_handle.set(validate_password(&input.value())); + } + }) + }; + + let onsubmit = Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + + let email_ref = input_password.clone(); + let password_ref = input_password.clone(); + let error_handle = error_handle.clone(); + console::log_1(&format!("Email: {}, Password: {}", input_email, input_password).into()); + + spawn_local(async move { + let email_val = email_ref.clone(); + let password_val = password_ref.clone(); + let error_handle = error_handle.clone(); + if email_valid && password_valid { + let response = login_user(email_val.to_string(), password_val.to_string()).await; + match response { + Ok(_) => { + console::log_1(&"success".into()); + let window: Window = web_sys::window().expect("window not available"); + let location = window.location(); + let _ = location.set_href("/error"); + } + Err(err) => { + error_handle.set(err); + } + } + } else { + error_handle.set("Please provide a valid email and password!".into()); + } + }); + }); + + let on_toggle_password = { + Callback::from(move |_| { + if eye_active { + password_type_handle.set("password".into()) + } else { + password_type_handle.set("text".into()) + } + eye_active_handle.set(!eye_active); + }) + }; + + html! { +
+ + +
+ } +} diff --git a/examples/tailwind/src/components/login_form_two.rs b/examples/tailwind/src/components/login_form_two.rs new file mode 100644 index 0000000..0f196fe --- /dev/null +++ b/examples/tailwind/src/components/login_form_two.rs @@ -0,0 +1,259 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use wasm_bindgen_futures::spawn_local; +use web_sys::{console, HtmlInputElement, Window}; +use yew::prelude::*; + +use crate::api::auth::login_user; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct LoginUserSchema { + email: String, + password: String, +} + +#[function_component(LoginFormTwo)] +pub fn login_form_two() -> Html { + let error_handle = use_state(String::default); + let error = (*error_handle).clone(); + + let email_valid_handle = use_state(|| true); + let email_valid = (*email_valid_handle).clone(); + + let eye_active_handle = use_state(|| false); + let eye_active = (*eye_active_handle).clone(); + + let password_valid_handle = use_state(|| true); + let password_valid = (*password_valid_handle).clone(); + + let password_type_handle = use_state(|| "text"); + let password_type = (*password_type_handle).clone(); + + let input_email_ref = use_node_ref(); + let input_email_handle = use_state(String::default); + let input_email = (*input_email_handle).clone(); + + let input_password_ref = use_node_ref(); + let input_password_handle = use_state(String::default); + let input_password = (*input_password_handle).clone(); + + let validate_email = |email: &str| { + let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap(); + pattern.is_match(email) + }; + + let validate_password = |password: &str| !password.is_empty(); + + let on_email_change = { + let input_email_ref = input_email_ref.clone(); + + Callback::from(move |_| { + let input = input_email_ref.cast::(); + + if let Some(input) = input { + let value = input.value(); + input_email_handle.set(value); + email_valid_handle.set(validate_email(&input.value())); + } + }) + }; + + let on_password_change = { + let input_password_ref = input_password_ref.clone(); + + Callback::from(move |_| { + let input = input_password_ref.cast::(); + + if let Some(input) = input { + let value = input.value(); + input_password_handle.set(value); + password_valid_handle.set(validate_password(&input.value())); + } + }) + }; + + let onsubmit = Callback::from(move |event: SubmitEvent| { + event.prevent_default(); + + let email_ref = input_password.clone(); + let password_ref = input_password.clone(); + let error_handle = error_handle.clone(); + console::log_1(&format!("Email: {}, Password: {}", input_email, input_password).into()); + + spawn_local(async move { + let email_val = email_ref.clone(); + let password_val = password_ref.clone(); + let error_handle = error_handle.clone(); + if email_valid && password_valid { + let response = login_user(email_val.to_string(), password_val.to_string()).await; + match response { + Ok(_) => { + console::log_1(&"success".into()); + let window: Window = web_sys::window().expect("window not available"); + let location = window.location(); + let _ = location.set_href("/error"); + } + Err(err) => { + error_handle.set(err); + } + } + } else { + error_handle.set("Please provide a valid email and password!".into()); + } + }); + }); + + let on_toggle_password = { + Callback::from(move |_| { + if eye_active { + password_type_handle.set("password".into()) + } else { + password_type_handle.set("text".into()) + } + eye_active_handle.set(!eye_active); + }) + }; + + html! { +
+
+
+
+
+
+
+
+

+ {"Welcome Back"} +

+

+ {"Fill in your credentials below to log in!"} +

+
+ if !error.is_empty() { +
{error}
+ } +
+
+
+ +
+ +
+
+ if !email_valid { +
{"Enter a valid email address"}
+ } +
+
+
+ +
+ + +
+
+ if !password_valid { +
{"Password can't be blank"}
+ } + +
+ +
+
+
+

{"Don't have an account?"}

+ {"Sign Up"} +
+ +
+
+
+
+
+ } +} diff --git a/examples/tailwind/src/components/mod.rs b/examples/tailwind/src/components/mod.rs new file mode 100644 index 0000000..3ee5309 --- /dev/null +++ b/examples/tailwind/src/components/mod.rs @@ -0,0 +1,6 @@ +pub mod common; +pub mod contact_form_one; +pub mod login_form_one; +pub mod login_form_three; +pub mod login_form_two; +pub mod multi_step_form_one; diff --git a/examples/tailwind/src/components/multi_step_form_one.rs b/examples/tailwind/src/components/multi_step_form_one.rs new file mode 100644 index 0000000..59f0c12 --- /dev/null +++ b/examples/tailwind/src/components/multi_step_form_one.rs @@ -0,0 +1,392 @@ +use crate::components::common::{validate_email, validate_input}; +use input_yew::CustomInput; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +#[function_component(MultiStepFormOne)] +pub fn multi_step_form_one() -> Html { + // TODO: Figure out how to refactor all of this mess! + let error_handle = use_state(String::default); + let error = (*error_handle).clone(); + + let email_valid_handle = use_state(|| true); + let email_valid = (*email_valid_handle).clone(); + + let full_name_valid_handle = use_state(|| true); + let full_name_valid = (*full_name_valid_handle).clone(); + + let phone_number_valid_handle = use_state(|| true); + let phone_number_valid = (*phone_number_valid_handle).clone(); + + let address_valid_handle = use_state(|| true); + let address_valid = (*address_valid_handle).clone(); + + let birthday_valid_handle = use_state(|| true); + let birthday_valid = (*birthday_valid_handle).clone(); + + let gender_valid_handle = use_state(|| true); + let gender_valid = (*gender_valid_handle).clone(); + + let username_valid_handle = use_state(|| true); + let username_valid = (*username_valid_handle).clone(); + + let password_valid_handle = use_state(|| true); + let password_valid = (*password_valid_handle).clone(); + + let input_email_ref = use_node_ref(); + let input_email_handle = use_state(String::default); + + let input_full_name_ref = use_node_ref(); + let input_full_name_handle = use_state(String::default); + + let input_subject_ref = use_node_ref(); + let input_subject_handle = use_state(String::default); + + let input_phone_number_ref = use_node_ref(); + let input_phone_number_handle = use_state(String::default); + + let input_address_ref = use_node_ref(); + let input_address_handle = use_state(String::default); + + let input_birthday_ref = use_node_ref(); + let input_birthday_handle = use_state(String::default); + + let input_gender_ref = use_node_ref(); + let input_gender_handle = use_state(String::default); + let input_gender = (*input_gender_handle).clone(); + + let input_username_ref = use_node_ref(); + let input_username_handle = use_state(String::default); + let input_username_number = (*input_username_handle).clone(); + + let input_password_ref = use_node_ref(); + let input_password_handle = use_state(String::default); + let input_password_number = (*input_password_handle).clone(); + + let current_step_handle = use_state(|| 0); + let current_step = (*current_step_handle).clone(); + + let on_next = { + let error_handle = error_handle.clone(); + let counter = current_step_handle.clone(); + move |_| match *counter { + 0 => { + if full_name_valid && email_valid { + let value = *counter + 1; + counter.set(value); + error_handle.set("".to_string()); + } else { + error_handle + .set("Please provide a valid full name and email address!".to_string()); + } + } + 1 => { + if phone_number_valid && address_valid { + let value = *counter + 1; + counter.set(value); + error_handle.set("".to_string()); + } else { + error_handle + .set("Please provide a valid phone number and address!".to_string()); + } + } + 2 => { + if birthday_valid && gender_valid { + let value = *counter + 1; + counter.set(value); + error_handle.set("".to_string()); + } else { + error_handle.set("Please provide a valid birth date and gender!".to_string()); + } + } + _ => println!("Ain't special"), + } + }; + + let on_previous = { + let counter = current_step_handle.clone(); + move |_| { + let value = *counter - 1; + counter.set(value); + } + }; + + let on_gender_change = { + let input_gender_ref = input_gender_ref.clone(); + + Callback::from(move |_| { + let input = input_gender_ref.cast::(); + + if let Some(input) = input { + let value = input.value(); + input_gender_handle.set(value); + gender_valid_handle.set(validate_input(input.value())); + } + }) + }; + + // TODO: add an onclick handler on last button + + let render_progress_item = |(index, _): (usize, &str)| { + // TODO: Refactor and convert to a for loop/map, make this tag show up: + html! { +
+
+

{"Step 1"}

+
+
+ {"1"} + if index > 0 { + + } +
+ if index > 0 { + + } + else { + + } +
+
+
+

{"Step 2"}

+
+
+ {"2"} + if index > 1 { + + } +
+ if index > 1 { + + } + else { + + } +
+
+
+

{"Step 3"}

+
+
+ {"3"} + if index > 2 { + + } +
+ if index > 2 { + + } + else { + + } +
+
+
+

{"Step 4"}

+
+
+ {"4"} +
+
+
+
+ } + }; + + let current_step_content = match current_step { + 0 => html! { +
+
{"Personal Information"}
+ + + +
+ }, + 1 => html! { +
+
{"Contact Details"}
+ + +
+ + +
+
+ }, + 2 => html! { +
+
{"Date of Birth"}
+ +
+
{"Gender"}
+ +
+ if !gender_valid { +
{"Gender can't be blank!"}
+ } +
+ + +
+
+ }, + 3 => html! { +
+
{"Account Details"}
+ + +
+ + +
+
+ }, + _ => html! {}, + }; + + html! { +
+
+ if !error.is_empty() { +
{error}
+ } +
{"Multi-Step Form"}
+
+ { render_progress_item((current_step, ""))} +
+
+ { current_step_content } +
+
+
+ } +} diff --git a/examples/tailwind/src/main.rs b/examples/tailwind/src/main.rs new file mode 100644 index 0000000..1a8e00f --- /dev/null +++ b/examples/tailwind/src/main.rs @@ -0,0 +1,9 @@ +mod api; +mod app; +mod components; +mod pages; +mod router; + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/examples/tailwind/src/pages/contact_page_one.rs b/examples/tailwind/src/pages/contact_page_one.rs new file mode 100644 index 0000000..995843b --- /dev/null +++ b/examples/tailwind/src/pages/contact_page_one.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +use crate::components::contact_form_one::ContactFormOne; + +#[function_component(ContactPageOne)] +pub fn contact_page_one() -> Html { + html! { + + } +} diff --git a/examples/tailwind/src/pages/error.rs b/examples/tailwind/src/pages/error.rs new file mode 100644 index 0000000..a9ebf18 --- /dev/null +++ b/examples/tailwind/src/pages/error.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +#[function_component(Error)] +pub fn error() -> Html { + html! { +
+

{ "Error Page." }

+
+ } +} diff --git a/examples/tailwind/src/pages/login_page_one.rs b/examples/tailwind/src/pages/login_page_one.rs new file mode 100644 index 0000000..bdbcbd1 --- /dev/null +++ b/examples/tailwind/src/pages/login_page_one.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +use crate::components::login_form_one::LoginFormOne; + +#[function_component(LoginPageOne)] +pub fn login_page_one() -> Html { + html! { + + } +} diff --git a/examples/tailwind/src/pages/login_page_three.rs b/examples/tailwind/src/pages/login_page_three.rs new file mode 100644 index 0000000..a0392d7 --- /dev/null +++ b/examples/tailwind/src/pages/login_page_three.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +use crate::components::login_form_three::LoginFormThree; + +#[function_component(LoginPageThree)] +pub fn login_page_three() -> Html { + html! { + + } +} diff --git a/examples/tailwind/src/pages/login_page_two.rs b/examples/tailwind/src/pages/login_page_two.rs new file mode 100644 index 0000000..b412543 --- /dev/null +++ b/examples/tailwind/src/pages/login_page_two.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +use crate::components::login_form_two::LoginFormTwo; + +#[function_component(LoginPageTwo)] +pub fn login_page_two() -> Html { + html! { + + } +} diff --git a/examples/tailwind/src/pages/mod.rs b/examples/tailwind/src/pages/mod.rs new file mode 100644 index 0000000..0eccebb --- /dev/null +++ b/examples/tailwind/src/pages/mod.rs @@ -0,0 +1,6 @@ +pub mod contact_page_one; +pub mod error; +pub mod login_page_one; +pub mod login_page_three; +pub mod login_page_two; +pub mod multi_step_page_one; diff --git a/examples/tailwind/src/pages/multi_step_page_one.rs b/examples/tailwind/src/pages/multi_step_page_one.rs new file mode 100644 index 0000000..6aa3aa7 --- /dev/null +++ b/examples/tailwind/src/pages/multi_step_page_one.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; + +use crate::components::multi_step_form_one::MultiStepFormOne; + +#[function_component(MultiStepPageOne)] +pub fn multi_step_page_one() -> Html { + html! { + + } +} diff --git a/examples/tailwind/src/router.rs b/examples/tailwind/src/router.rs new file mode 100644 index 0000000..6fdd0bb --- /dev/null +++ b/examples/tailwind/src/router.rs @@ -0,0 +1,36 @@ +use yew::prelude::*; +use yew_router::prelude::*; + +use crate::pages::contact_page_one::ContactPageOne; +use crate::pages::error::Error; +use crate::pages::login_page_one::LoginPageOne; +use crate::pages::login_page_three::LoginPageThree; +use crate::pages::login_page_two::LoginPageTwo; +use crate::pages::multi_step_page_one::MultiStepPageOne; + +#[derive(Clone, Routable, PartialEq)] +pub enum Route { + #[at("/error")] + Error, + #[at("/login/1")] + LoginPageOne, + #[at("/login/2")] + LoginPageTwo, + #[at("/login/3")] + LoginPageThree, + #[at("/contact/1")] + ContactPageOne, + #[at("/multi-step/1")] + MultiStepPageOne, +} + +pub fn switch(routes: Route) -> Html { + match routes { + Route::LoginPageOne => html! { }, + Route::LoginPageTwo => html! { }, + Route::LoginPageThree => html! { }, + Route::ContactPageOne => html! { }, + Route::MultiStepPageOne => html! { }, + Route::Error => html! { }, + } +} diff --git a/examples/tailwind/tailwind.config.js b/examples/tailwind/tailwind.config.js new file mode 100644 index 0000000..88f1dfd --- /dev/null +++ b/examples/tailwind/tailwind.config.js @@ -0,0 +1,42 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./src/**/*.{rs,html}" + ], + theme: { + extend: { + colors: { + 'ct-dark-600': '#222', + 'ct-dark-200': '#e5e7eb', + 'ct-dark-100': '#f5f6f7', + 'ct-blue-600': '#2363eb', + 'ct-yellow-600': '#f9d13e', + 'ct-red-500': '#ef4444', + }, + fontFamily: { + sans: ['Roboto', 'sans-serif'], + serif: ['Roboto', 'serif'], + }, + container: { + center: true, + padding: '1rem', + screens: { + sm: '480px', + md: '768px', + lg: '976px', + xl: '1440px', + }, + spacing: { + '128': '32rem', + '144': '36rem', + }, + borderRadius: { + '4xl': '2rem', + } + }, + }, + }, + plugins: [], +} + diff --git a/src/lib.rs b/src/lib.rs index b158503..1b8d6de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,51 +8,51 @@ use yew::prelude::*; #[derive(Properties, PartialEq, Clone)] pub struct Props { /// The type of the input, e.g., "text", "password", etc. - #[prop_or(AttrValue::from("text"))] - pub input_type: AttrValue, + #[prop_or("text")] + pub input_type: &'static str, /// The label to be displayed for the input field. #[prop_or_default] - pub label: AttrValue, + pub label: &'static str, /// The name of the input field, used for form submission and accessibility. #[prop_or_default] - pub name: AttrValue, + pub name: &'static str, /// Indicates whether the input is required or not. #[prop_or_default] - pub required: Option, + pub required: bool, /// A reference to the DOM node of the input element. pub input_ref: NodeRef, /// The error message to display when there is a validation error. #[prop_or_default] - pub error_message: AttrValue, + pub error_message: &'static str, /// The CSS class to be applied to all inner elements. #[prop_or_default] - pub form_input_class: AttrValue, + pub form_input_class: &'static str, /// The CSS class to be applied to the inner input element and icon. #[prop_or_default] - pub form_input_field_class: AttrValue, + pub form_input_field_class: &'static str, /// The CSS class to be applied to the label for the input element. #[prop_or_default] - pub form_input_label_class: AttrValue, + pub form_input_label_class: &'static str, /// The CSS class to be applied to the input element. #[prop_or_default] - pub form_input_input_class: AttrValue, + pub form_input_input_class: &'static str, /// The CSS class to be applied to the error div element. #[prop_or_default] - pub form_input_error_class: AttrValue, + pub form_input_error_class: &'static str, /// The CSS class to be applied to the icon element. #[prop_or_default] - pub icon_class: AttrValue, + pub icon_class: &'static str, /// The state handle for managing the value of the input. pub input_handle: UseStateHandle, @@ -64,37 +64,37 @@ pub struct Props { pub validate_function: Callback, /// The icon when the password is visible. Assuming fontawesome icons is used by default. - #[prop_or(AttrValue::from("fa fa-eye"))] - pub eye_active: AttrValue, + #[prop_or("fa fa-eye")] + pub eye_active: &'static str, /// The icon when the password is not visible. Assuming fontawesome icons is used by default. - #[prop_or(AttrValue::from("fa fa-eye-slash"))] - pub eye_disabled: AttrValue, + #[prop_or("fa fa-eye-slash")] + pub eye_disabled: &'static str, // Additional props for accessibility and SEO: /// The ID attribute of the input element. #[prop_or_default] - pub input_id: AttrValue, + pub input_id: &'static str, /// The placeholder text to be displayed in the input element. #[prop_or_default] - pub input_placeholder: AttrValue, + pub input_placeholder: &'static str, /// The aria-label attribute for screen readers, providing a label for accessibility. #[prop_or_default] - pub aria_label: AttrValue, + pub aria_label: &'static str, /// The aria-required attribute for screen readers, indicating whether the input is required. - #[prop_or(AttrValue::from("true"))] - pub aria_required: AttrValue, + #[prop_or("true")] + pub aria_required: &'static str, /// The aria-invalid attribute for screen readers, indicating whether the input value is invalid. - #[prop_or(AttrValue::from("true"))] - pub aria_invalid: AttrValue, + #[prop_or("true")] + pub aria_invalid: &'static str, /// The aria-describedby attribute for screen readers, describing the input element's error message. #[prop_or_default] - pub aria_describedby: AttrValue, + pub aria_describedby: &'static str, } /// custom_input_component @@ -139,21 +139,21 @@ pub struct Props { /// #[function_component(LoginFormOne)] /// pub fn login_form_one() -> Html { /// let error_handle = use_state(String::default); -/// let error = (*error_handle).clone(); +/// let error = (*error_handle).clone();; /// /// let email_valid_handle = use_state(|| true); -/// let email_valid = (*email_valid_handle).clone(); +/// let email_valid = (*email_valid_handle).clone();; /// /// let password_valid_handle = use_state(|| true); -/// let password_valid = (*password_valid_handle).clone(); +/// let password_valid = (*password_valid_handle).clone();; /// /// let input_email_ref = use_node_ref(); /// let input_email_handle = use_state(String::default); -/// let input_email = (*input_email_handle).clone(); +/// let input_email = (*input_email_handle).clone();; /// /// let input_password_ref = use_node_ref(); /// let input_password_handle = use_state(String::default); -/// let input_password = (*input_password_handle).clone(); +/// let input_password = (*input_password_handle).clone();; /// /// let onsubmit = Callback::from(move |event: SubmitEvent| { /// event.prevent_default(); @@ -175,29 +175,29 @@ pub struct Props { /// ///
/// /// Html { let input_valid = *props.input_valid_handle; - let aria_invalid = props.aria_invalid.clone(); + let aria_invalid = props.aria_invalid; - let eye_icon_active = props.eye_active.clone(); + let eye_icon_active = props.eye_active; - let eye_icon_disabled = props.eye_disabled.clone(); + let eye_icon_disabled = props.eye_disabled; - let aria_required = props.aria_required.clone(); + let aria_required = props.aria_required; - let input_type = props.input_type.clone(); + let input_type = props.input_type; let onchange = { let input_ref = props.input_ref.clone(); @@ -272,7 +272,7 @@ pub fn custom_input(props: &Props) -> Html { let on_phone_number_input = { let input_ref = props.input_ref.clone(); let input_handle = props.input_handle.clone(); - let country_handle = country_handle.clone(); + let country_handle = country_handle; Callback::from(move |_| { if let Some(input) = input_ref.cast::() { for (code, _, _, _, _, _) in &COUNTRY_CODES { @@ -305,105 +305,99 @@ pub fn custom_input(props: &Props) -> Html { <> + /> }, "textarea" => html! { - +