feat: sweeping improvements to home_page.rs
Completely refactors the ownership of all data such that server resources are loaded into a signal asynchronously, then displayed in the form properly. All button handlers are also now separately defined functions, instead of being inline.
This commit is contained in:
1 parent
74e7111820
commit
85ec857e38
4 files changed
+243
-306
No files matched your search
+76
-126
@@ -1,136 +1,86 @@
|
|||||||
Forms should be built for this application with a series of JSON-type files.
|
To track errors and force form input validation, we're going to create a custom `ValidatedForm` component.
|
||||||
Each form will have a series of sections, and each section will have a series of
|
|
||||||
questions. Each question has a type and a label - question styles may come later.
|
|
||||||
It may be possible for us to call out specific question types without needing
|
|
||||||
to parse the types separately, but that feels not-really-worth-it.
|
|
||||||
|
|
||||||
Example:
|
This component will do two things:
|
||||||
|
- Provide a submit button (that is disabled if the form is invalid, still thinking about that)
|
||||||
|
- Define a validation method that will be called on submit that's updated on a per-input basis
|
||||||
|
(this likely looks like a `Vec<RwSignal<ValidationState>>`)
|
||||||
|
|
||||||
```
|
In order for this to work, the form will `provide_context` to the `Vec`, and then it will check this
|
||||||
{
|
vector on submit. It doesn't worry about the states inbetween, only during the submission logic.
|
||||||
"SectionDefinitions": [
|
|
||||||
"SectionTitle": [
|
|
||||||
"text": {
|
|
||||||
"label": "First name question!",
|
|
||||||
"placeholder": "John",
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"label": "Last name question!",
|
|
||||||
"placeholder": "Smith",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Section Two": [
|
|
||||||
"text": {
|
|
||||||
"label": "Section two testing question",
|
|
||||||
"placeholder": "Some example here",
|
|
||||||
},
|
|
||||||
"select": {
|
|
||||||
"label": "This is a dropdown element!",
|
|
||||||
"placeholder": "option1",
|
|
||||||
"possibleValues": [
|
|
||||||
"option1": "something",
|
|
||||||
"option2": "another something",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
...and so on. Here's a likely set of structures to use:
|
The inputs start off with a default state, which will be defined by that input's validation method.
|
||||||
|
This means that every input will run the validation method on mount and on each change. The first time the
|
||||||
|
function is called should be during mount, which will set the default state. This does mean that any inputs
|
||||||
|
that are marked as required will always be invalid on mount, and so we need to determine when these
|
||||||
|
error messages should be shown and updated.
|
||||||
|
|
||||||
```
|
I think they should be shown in these situations:
|
||||||
struct FormDefinitionDoc {
|
- Text inputs: on blur - we don't want to scream at users as they type.
|
||||||
Vec<Section> section_defs;
|
- Selectable inputs: on change and form submit - only show the message if the value is invalid, and not at mount.
|
||||||
// and any extra values that should be applied
|
|
||||||
// across the entire form, like settings.
|
To check values for the selectable inputs at submit time, we'll probably need to provide a refresh method
|
||||||
|
that can be called on submit. This method must somehow be callable from the outside of the component and
|
||||||
|
will show the error messages. We'd likely store all this in a struct of some kind, which could contain an
|
||||||
|
`error` and a `show_message` signal.
|
||||||
|
|
||||||
|
We already know that we can send any `Signal` to another component, so we can just send the `show_message`
|
||||||
|
signal to the parent component without any issues. We'll just use that signal to show the error messages
|
||||||
|
in the component as well. Usually we don't want to edit too many signals outside their local context,
|
||||||
|
but this is a special case – documentation is therefore incredibly important! Remember that we may also
|
||||||
|
edit the `show_message` signal within its local context as well, so we need to make sure there are no cyclical
|
||||||
|
dependencies. Alternatively, we could not edit the `show_message` signal at all locally and just use it
|
||||||
|
to trigger side effects that show messages. Have to think about this more before implementation.
|
||||||
|
|
||||||
|
Here's a rough sketch of it all put together:
|
||||||
|
```rust
|
||||||
|
enum ValidationState {
|
||||||
|
/// A valid input!
|
||||||
|
Valid,
|
||||||
|
/// An invalid input with an error message.
|
||||||
|
Invalid(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Section {
|
// We'll store this in a Vec<RwSignal<InputState>>
|
||||||
Vec<QuestionComponent> questions;
|
struct InputState {
|
||||||
// each section may have a series of settings including how to submit,
|
/// Determines if there is an error and provides an error message.
|
||||||
// when to submit, and where to submit to at the very least.
|
error: Signal<ValidationState>,
|
||||||
|
/// Determines if the form has requested to show error messages.
|
||||||
|
show_message: RwSignal<bool>
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QuestionComponent {
|
// Define a new type for ease of use.
|
||||||
label: String,
|
type ValidationVec = Vec<RwSignal<InputState>>;
|
||||||
placeholder: String,
|
|
||||||
possible_values: HashMap<str, str>,
|
// Parent form component
|
||||||
// there are definitely values that i'll find while creating this structure.
|
#[component]
|
||||||
|
fn Form() -> impl IntoView {
|
||||||
|
let validation_states: ValidationVec = Vec::new();
|
||||||
|
|
||||||
|
provide_context(validation_states);
|
||||||
|
|
||||||
|
/* All other form logic */
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validation_function(input: String) -> ValidationState {
|
||||||
|
// Validation logic is specific to each input type.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child component
|
||||||
|
#[component]
|
||||||
|
fn AnyInput() -> impl IntoView {
|
||||||
|
let validation_states = use_context::<ValidationVec>().expect("Couldn't find form validation context");
|
||||||
|
let current_input = RwSignal::new(String::new()); // Obviously, this would have a default value.
|
||||||
|
let error_signal = RwSignal::new(validation_function(current_input.get()));
|
||||||
|
let show_errors = RwSignal::new(false); // Don't show them on mount!
|
||||||
|
|
||||||
|
// See below for InputState definition
|
||||||
|
let error_state = InputState {
|
||||||
|
error: error_signal.clone().into(),
|
||||||
|
show_message: show_errors.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
validation_states.push(RwSignal::new())
|
||||||
|
|
||||||
|
/* All other input logic */
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Types of questions that are required, starting with the providers.
|
|
||||||
|
|
||||||
Providers must be able to:
|
|
||||||
|
|
||||||
- Log into the website using a username and password (handled by AWS)
|
|
||||||
- Create and manage multiple scholarships.
|
|
||||||
- Input scholarship information for each scholarship.
|
|
||||||
- View students that have applied.
|
|
||||||
- View student application information (should just be labels??)
|
|
||||||
- Buttons are a given. Duh.
|
|
||||||
|
|
||||||
Students must be able to:
|
|
||||||
|
|
||||||
- Log in using their Region 15 email (handled by AWS)
|
|
||||||
- Create and manage their general application.
|
|
||||||
- Create and manage applications specific to a single scholarship.
|
|
||||||
- View their acceptance status (need more confirmation)
|
|
||||||
|
|
||||||
Administrators must be able to:
|
|
||||||
|
|
||||||
- Log in using their Region 15 email (handled by AWS)
|
|
||||||
- Manage student scholarship applications.
|
|
||||||
- Manage scholarship provider account information.
|
|
||||||
- Create/manage scholarship information.
|
|
||||||
- Download student applications if needed.
|
|
||||||
- Statistics would be nice, but are likely a future thing.
|
|
||||||
|
|
||||||
Right off the bad, I think I can see a few types of buttons and text inputs,
|
|
||||||
along with dropdowns, checkboxes, our custom `MultiEntry` element from last
|
|
||||||
time. That means no new options, but it does mean that the `FormDefinition`
|
|
||||||
needs to be able to keep track of all this information.
|
|
||||||
|
|
||||||
These are some example `QuestionComponent` objects:
|
|
||||||
|
|
||||||
Text fields:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
question_type: "TEXT" | "EMAIL" | "NUMBER",
|
|
||||||
label: "The question label",
|
|
||||||
name: "input_name[jsdoc]",
|
|
||||||
placeholder: "An example input"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Dropdowns/checkboxes/radios:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
question_type: "DROPDOWN" | "CHECKBOX" | "RADIO",
|
|
||||||
label: "The question label",
|
|
||||||
name: "input_name[jsdoc]",
|
|
||||||
placeholder: "The pre-selected option value",
|
|
||||||
options: [
|
|
||||||
"optionValue": "Option Text"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`MultiEntry`:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
question_type: "MULTI",
|
|
||||||
label: "The question label",
|
|
||||||
name: "input_name[jsdoc]"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The real question here: how can we store this information in a database? And
|
|
||||||
this means specifically within DynamoDB, because this is not an easy structure
|
|
||||||
to store in there.
|
|
||||||
That's going to be the hardest apart and I'll likely involve the AI in that
|
|
||||||
planning.
|
|
||||||
@@ -9,7 +9,7 @@ pub fn Checkbox(
|
|||||||
#[prop(into)] on_change: Callback<(), ()>,
|
#[prop(into)] on_change: Callback<(), ()>,
|
||||||
#[prop(into)] value: String,
|
#[prop(into)] value: String,
|
||||||
#[prop(into)] name: String,
|
#[prop(into)] name: String,
|
||||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
#[prop(into)] disabled: Signal<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let id = create_unique_id(&name, &value);
|
let id = create_unique_id(&name, &value);
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ pub fn CheckboxList(
|
|||||||
#[prop(into)] data_member: String,
|
#[prop(into)] data_member: String,
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop()] items: Vec<String>,
|
#[prop()] items: Vec<String>,
|
||||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub fn Select(
|
|||||||
#[prop(into)] data_member: String,
|
#[prop(into)] data_member: String,
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let on_change = {
|
let on_change = {
|
||||||
let data_member = data_member.clone();
|
let data_member = data_member.clone();
|
||||||
|
|||||||
+164
-177
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
// Server dependencies
|
// Server dependencies
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||||
@@ -17,10 +18,10 @@ use leptos::leptos_dom::logging::console_log;
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use leptos::logging::log;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos::web_sys::HtmlAnchorElement;
|
use leptos::web_sys::HtmlAnchorElement;
|
||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use traits::{AsReactive, ReactiveCapture};
|
|
||||||
use crate::pages::utils::get_user_claims;
|
use crate::pages::utils::get_user_claims;
|
||||||
|
|
||||||
/// # Get Student Info
|
/// # Get Student Info
|
||||||
@@ -141,189 +142,50 @@ pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
|||||||
/// The main home page component. Contains a simple contact form.
|
/// The main home page component. Contains a simple contact form.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn HomePage() -> impl IntoView {
|
pub fn HomePage() -> impl IntoView {
|
||||||
// Creates a reactive value to update the button
|
//#region Server Resources
|
||||||
let user_claims = get_user_claims();
|
let user_claims = get_user_claims();
|
||||||
|
let user_subject = Memo::new(move |_| {
|
||||||
|
user_claims.get()
|
||||||
|
.map(|claim| claim.claims.subject.clone())
|
||||||
|
});
|
||||||
|
|
||||||
// Note that the value passed in MUST be equatable.
|
let form_data = RwSignal::new(HashMap::new());
|
||||||
// We get/unwrap the value repeatedly until we get a simple string value, then clone it so that
|
|
||||||
// we don't lose access to it in the future, should we need it again.
|
let server_resource = Resource::new(
|
||||||
let server_resource: Resource<ExpandableInfo> = Resource::new(
|
move || user_subject.get(),
|
||||||
move || user_claims.get().map(|claim| claim.claims.subject.clone()),
|
async move |subject| get_submission(subject.unwrap_or_default()).await
|
||||||
async |opt_username| match opt_username {
|
|
||||||
Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| {
|
|
||||||
console_log(e.to_string().as_str());
|
|
||||||
ExpandableInfo::new(subject)
|
|
||||||
}),
|
|
||||||
None => ExpandableInfo::new("".to_owned()),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||||
|
|
||||||
view! {
|
Effect::new(move || {
|
||||||
<AuthLoaded fallback=Loading>
|
match server_resource.get() {
|
||||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
Some(Ok(submission)) => {
|
||||||
// Replace this fallback with a real loading screen.
|
form_data.set(submission.data);
|
||||||
<Suspense fallback=Loading>
|
|
||||||
<ValidatedForm on_submit=Callback::new(move |_| {})>
|
|
||||||
{move || {
|
|
||||||
server_resource
|
|
||||||
.get()
|
|
||||||
.map(|submission| {
|
|
||||||
let expandable_react = submission.as_reactive();
|
|
||||||
let elements_disabled = RwSignal::new(false);
|
|
||||||
let result_msg = Signal::derive(move || {
|
|
||||||
if submit_action.pending().get() {
|
|
||||||
elements_disabled.set(true);
|
|
||||||
"Sending request...".to_owned()
|
|
||||||
} else {
|
|
||||||
elements_disabled.set(false);
|
|
||||||
if let Some(result) = submit_action.value().get() {
|
|
||||||
match result {
|
|
||||||
Ok(_) => "Request sent successfully.".to_owned(),
|
|
||||||
Err(e) => format!("Failed to send request: {:?}", e),
|
|
||||||
}
|
}
|
||||||
} else {
|
Some(Err(err)) => {
|
||||||
"".to_owned()
|
log!("Error while getting information: {:?}", err);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log!("An unknown error occurred.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view! {
|
//#endregion
|
||||||
<Row>
|
//#region Component State
|
||||||
<div class="flex flex-col flex-1" />
|
let elements_disabled = Signal::derive(move || {
|
||||||
<Panel>
|
submit_action.pending().get()
|
||||||
<Row>
|
|
||||||
<h1 class="text-3xl font-bold">
|
|
||||||
"Region 15 Scholarship Application (DEMO)"
|
|
||||||
</h1>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<p class="text-sm">
|
|
||||||
"This is a very simple demonstration of the scholarship application written in Rust."
|
|
||||||
</p>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
// Text { data_member, label, placeholder }
|
|
||||||
<OutlinedTextField
|
|
||||||
label="First Name:"
|
|
||||||
placeholder="John"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="first_name"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
<OutlinedTextField
|
|
||||||
label="Last Name:"
|
|
||||||
placeholder="Smith"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="last_name"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<OutlinedTextField
|
|
||||||
label="Contact Email:"
|
|
||||||
placeholder="student@region15.org"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="contact_email"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
input_type=TextFieldType::Email
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<OutlinedTextField
|
|
||||||
label="Phone Number:"
|
|
||||||
placeholder="123-456-7890"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="phone_number"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<OutlinedTextField
|
|
||||||
label="Street Address:"
|
|
||||||
placeholder="123 Fake Street"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="address"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<OutlinedTextField
|
|
||||||
label="Highest Math SAT Score:"
|
|
||||||
placeholder="600"
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="math_sat"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
input_type=TextFieldType::Number
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
// Radio { data_member, label, items }
|
|
||||||
<RadioList
|
|
||||||
label="Town:"
|
|
||||||
items=vec!["Southbury", "Middlebury"]
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| s.into())
|
|
||||||
.collect()
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="town"
|
|
||||||
data_map = expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
// Checkbox { data_member, label, items }
|
|
||||||
<CheckboxList
|
|
||||||
label="Favorite Candies:"
|
|
||||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| s.into())
|
|
||||||
.collect()
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="favorite_candies"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
// Select { data_member, label, items }
|
|
||||||
<Select
|
|
||||||
label="Gender:"
|
|
||||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| s.to_owned())
|
|
||||||
.collect()
|
|
||||||
disabled=elements_disabled
|
|
||||||
data_member="gender"
|
|
||||||
data_map=expandable_react.data
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<MultiEntry
|
|
||||||
data_map=expandable_react.data
|
|
||||||
data_member="community_involvement"
|
|
||||||
schema=vec![
|
|
||||||
input!(Text, "service_name", "Activity Name:", "Some service activity..."),
|
|
||||||
input!(Number, "service_hours", "Total Service Hours", "20")
|
|
||||||
]
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
|
||||||
<ActionButton
|
|
||||||
on:click=move |_| {
|
|
||||||
let captured_map = expandable_react.capture();
|
|
||||||
console_log(
|
|
||||||
format!("Map values: {:?}", captured_map).as_str(),
|
|
||||||
);
|
|
||||||
submit_action
|
|
||||||
.dispatch(CreateSampleSubmission {
|
|
||||||
student_info: captured_map,
|
|
||||||
});
|
});
|
||||||
}
|
//#endregion
|
||||||
disabled=elements_disabled
|
//#region Event logic
|
||||||
>
|
let on_submit = move |_| {
|
||||||
"Submit"
|
let mut exp_info = ExpandableInfo::new(user_subject.get().unwrap_or_default());
|
||||||
</ActionButton>
|
exp_info.data = form_data.get();
|
||||||
</Row>
|
submit_action.dispatch(CreateSampleSubmission {
|
||||||
<Row>
|
student_info: exp_info
|
||||||
<ActionButton on:click=move |_| {
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let pdf_button_click = move |_| {
|
||||||
console_log("Attempting to get PDF from server endpoint");
|
console_log("Attempting to get PDF from server endpoint");
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let result = create_example_pdf().await;
|
let result = create_example_pdf().await;
|
||||||
@@ -345,18 +207,143 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
link.click();
|
link.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}>"Get PDF"</ActionButton>
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<AuthLoaded fallback=Loading>
|
||||||
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
|
// Replace this fallback with a real loading screen.
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
server_resource
|
||||||
|
.get()
|
||||||
|
.map(|submission| {
|
||||||
|
view! {
|
||||||
|
<Row>
|
||||||
|
<div class="flex flex-col flex-1" />
|
||||||
|
<Panel>
|
||||||
|
<ValidatedForm on_submit=Callback::new(on_submit)>
|
||||||
|
<Row>
|
||||||
|
<h1 class="text-3xl font-bold">
|
||||||
|
"Region 15 Scholarship Application (DEMO)"
|
||||||
|
</h1>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<p>{result_msg}</p>
|
<p class="text-sm">
|
||||||
|
"This is a very simple demonstration of the scholarship application written in Rust."
|
||||||
|
</p>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="First Name:"
|
||||||
|
placeholder="John"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="first_name"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Last Name:"
|
||||||
|
placeholder="Smith"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="last_name"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Contact Email:"
|
||||||
|
placeholder="student@region15.org"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="contact_email"
|
||||||
|
data_map=form_data
|
||||||
|
input_type=TextFieldType::Email
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Phone Number:"
|
||||||
|
placeholder="123-456-7890"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="phone_number"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Street Address:"
|
||||||
|
placeholder="123 Fake Street"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="address"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label="Highest Math SAT Score:"
|
||||||
|
placeholder="600"
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="math_sat"
|
||||||
|
data_map=form_data
|
||||||
|
input_type=TextFieldType::Number
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<RadioList
|
||||||
|
label="Town:"
|
||||||
|
items=vec!["Southbury", "Middlebury"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.into())
|
||||||
|
.collect()
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="town"
|
||||||
|
data_map = form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<CheckboxList
|
||||||
|
label="Favorite Candies:"
|
||||||
|
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.into())
|
||||||
|
.collect()
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="favorite_candies"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Select
|
||||||
|
label="Gender:"
|
||||||
|
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.collect()
|
||||||
|
disabled=elements_disabled
|
||||||
|
data_member="gender"
|
||||||
|
data_map=form_data
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<MultiEntry
|
||||||
|
data_map=form_data
|
||||||
|
data_member="community_involvement"
|
||||||
|
schema=vec![
|
||||||
|
input!(Text, "service_name", "Activity Name:", "Some service activity..."),
|
||||||
|
input!(Number, "service_hours", "Total Service Hours", "20")
|
||||||
|
]
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</ValidatedForm>
|
||||||
</Panel>
|
</Panel>
|
||||||
<div class="flex flex-col flex-1" />
|
<div class="flex flex-col flex-1" />
|
||||||
</Row>
|
</Row>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</ValidatedForm>
|
<Row>
|
||||||
|
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
|
||||||
|
</Row>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
</AuthLoaded>
|
</AuthLoaded>
|
||||||
|
|||||||
Reference in new issue
Block a user