chore: pc change
This commit is contained in:
1 parent
21ba5e45ff
commit
f7ca1ecbc5
27 files changed
+926
-262
No files matched your search
+46
-5
@@ -1,15 +1,15 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
|
||||
use leptos_oidc::{
|
||||
Auth, AuthLoaded, AuthParameters, AuthSignal, Authenticated, Challenge, LoginLink, LogoutLink,
|
||||
};
|
||||
use leptos_router::{
|
||||
components::{Route, Router, Routes},
|
||||
StaticSegment,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::pages::{
|
||||
AboutPage,
|
||||
HomePage,
|
||||
ProviderEntry
|
||||
};
|
||||
use crate::pages::{AboutPage, HomePage, ProviderEntry};
|
||||
|
||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
view! {
|
||||
@@ -34,17 +34,38 @@ pub fn App() -> impl IntoView {
|
||||
// Provides context that manages stylesheets, titles, meta tags, etc.
|
||||
provide_meta_context();
|
||||
|
||||
// Authentication setup
|
||||
let parameters = AuthParameters {
|
||||
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(),
|
||||
client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(),
|
||||
redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(),
|
||||
post_logout_redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(),
|
||||
scope: Some("openid%20profile%20email".into()),
|
||||
audience: None,
|
||||
challenge: Challenge::None,
|
||||
};
|
||||
|
||||
let auth = Auth::signal();
|
||||
provide_context(auth); // allows use of this signal in lower areas of the tree without
|
||||
// explicitly passing it through the html tree
|
||||
|
||||
let _ = Auth::init(parameters);
|
||||
|
||||
view! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
<Stylesheet id="leptos" href="/pkg/scholarships-rs-wasm.css"/>
|
||||
|
||||
// <AuthLoading><p>"Authentication is loading"</p></AuthLoading>
|
||||
// <AuthErrorContext><AuthErrorView/></AuthErrorContext>
|
||||
|
||||
// sets the document title
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
// content for this welcome page
|
||||
<Router>
|
||||
<main>
|
||||
// TODO Create a 404 page
|
||||
<Routes fallback=|| "Page not found.".into_view()>
|
||||
<Route path=StaticSegment("") view=HomePage/>
|
||||
<Route path=StaticSegment("about") view=AboutPage/>
|
||||
@@ -54,3 +75,23 @@ pub fn App() -> impl IntoView {
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Unauthenticated() -> impl IntoView {
|
||||
view! {
|
||||
<p>"Error 401: Unauthorized"</p>
|
||||
<LoginLink class="text-login">"Sign in"</LoginLink>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn AuthErrorView() -> impl IntoView {
|
||||
let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found");
|
||||
|
||||
let error_message = move || auth.get().error().map(|error| format!("{error:?}"));
|
||||
|
||||
view! {
|
||||
<h1>"An authentication error occurred."</h1>
|
||||
{error_message}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FormDefinition {
|
||||
sections: Vec<Section>
|
||||
sections: Vec<Section>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Section {
|
||||
questions: Vec<QuestionType>
|
||||
questions: Vec<QuestionType>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -20,12 +19,10 @@ enum QuestionType {
|
||||
Dropdown,
|
||||
Checkbox,
|
||||
Radio,
|
||||
Multi
|
||||
Multi,
|
||||
}
|
||||
|
||||
struct TextInput {
|
||||
|
||||
}
|
||||
struct TextInput {}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct QuestionComponent {
|
||||
@@ -34,5 +31,5 @@ struct QuestionComponent {
|
||||
placeholder: Option<String>,
|
||||
|
||||
// Used in <select> and <checkbox> elements
|
||||
options: Option<HashMap<String, String>>
|
||||
options: Option<HashMap<String, String>>,
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoanerUser {
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub has_loan: bool
|
||||
pub has_loan: bool,
|
||||
}
|
||||
|
||||
impl LoanerUser {
|
||||
@@ -13,7 +12,7 @@ impl LoanerUser {
|
||||
first_name: "".to_string(),
|
||||
last_name: "".to_string(),
|
||||
email: "".to_string(),
|
||||
has_loan: false
|
||||
has_loan: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-6
@@ -1,8 +1,6 @@
|
||||
mod loaner_user;
|
||||
mod student_info;
|
||||
mod form_definition;
|
||||
mod loaner_user;
|
||||
mod student_info;
|
||||
mod user;
|
||||
|
||||
pub use self::{
|
||||
loaner_user::*,
|
||||
student_info::*
|
||||
};
|
||||
pub use self::{loaner_user::*, student_info::*, user::*};
|
||||
@@ -1,7 +1,9 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use macros::Reactive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Reactive)]
|
||||
pub struct StudentInfo {
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub math_sat_score: i32,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// API URL to get user information from Google's Groups API:
|
||||
// https://admin.googleapis.com/admin/directory/v1/groups?domain=region15.org&userKey=<email>
|
||||
// Note that this requires a service account of some type, and that it should only be called from
|
||||
// a pre-sign up lambda function.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserClaims {
|
||||
/// The time the JWT was issued, in UTC time.
|
||||
#[serde(rename = "iat")]
|
||||
issued_at: usize,
|
||||
/// The issuer of the JWT.
|
||||
#[serde(rename = "iss")]
|
||||
issuer: String,
|
||||
/// The time the JWT expires, in UTC time.
|
||||
#[serde(rename = "exp")]
|
||||
expiration_at: usize,
|
||||
/// The subject of the JWT. This is the only unique identifier for a user.
|
||||
#[serde(rename = "sub")]
|
||||
pub subject: String,
|
||||
/// The username of the user. This should be their email address.
|
||||
#[serde(rename = "username")]
|
||||
pub username: String,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Checkbox(
|
||||
#[prop()] checked: ReadSignal<bool>,
|
||||
#[prop()] on_change: Callback<bool>,
|
||||
#[prop()] label: String,
|
||||
#[prop()] id: String
|
||||
) -> impl IntoView {
|
||||
let label_id = id.clone();
|
||||
view! {
|
||||
<label for=label_id>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn Loading() -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<img src="loading.gif" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
mod action_button;
|
||||
mod text_field;
|
||||
mod loading;
|
||||
mod multi_entry;
|
||||
mod text_field;
|
||||
mod select;
|
||||
mod checkboxes;
|
||||
|
||||
pub use self::{
|
||||
action_button::*,
|
||||
text_field::*,
|
||||
multi_entry::*
|
||||
};
|
||||
pub use self::{action_button::*, loading::*, multi_entry::*, text_field::*, select::*};
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::components::ActionButton;
|
||||
use leptos::{leptos_dom::logging::console_log, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use crate::components::ActionButton;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct MultiEntryData {
|
||||
@@ -27,7 +27,7 @@ pub struct MultiEntryMember {
|
||||
/// The name of the member, used for display only.
|
||||
pub display_name: String,
|
||||
/// The actual name of the member, used to get the data from an Entry struct.
|
||||
pub member_name: String
|
||||
pub member_name: String,
|
||||
}
|
||||
|
||||
impl MultiEntryMember {
|
||||
@@ -35,7 +35,7 @@ impl MultiEntryMember {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
display_name: String::new(),
|
||||
member_name: String::new()
|
||||
member_name: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ impl MultiEntryMember {
|
||||
pub fn from(display_name: String, member_name: String) -> Self {
|
||||
Self {
|
||||
display_name,
|
||||
member_name
|
||||
member_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl MultiEntryMember {
|
||||
pub fn from_str(display_name: &str, member_name: &str) -> Self {
|
||||
Self {
|
||||
display_name: display_name.into(),
|
||||
member_name: member_name.into()
|
||||
member_name: member_name.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,9 +85,6 @@ pub fn Entry(
|
||||
/// The secondary display member for this entry.
|
||||
#[prop()]
|
||||
info_member: Signal<MultiEntryMember>,
|
||||
/// The child components of this Entry view.
|
||||
#[prop()]
|
||||
children: Children
|
||||
) -> impl IntoView {
|
||||
let nameData = entry_data.data.get(&name_member.get().member_name).unwrap();
|
||||
let infoData = entry_data.data.get(&info_member.get().member_name).unwrap();
|
||||
@@ -111,17 +108,19 @@ pub fn MultiEntry(
|
||||
name_member: Signal<MultiEntryMember>,
|
||||
/// The secondary display member for each entry object.
|
||||
#[prop()]
|
||||
info_member: Signal<MultiEntryMember>
|
||||
info_member: Signal<MultiEntryMember>,
|
||||
) -> impl IntoView {
|
||||
// Creates an entry in the given entries list.
|
||||
let create_entry = move |_| {
|
||||
let mut new_entry = MultiEntryData::new();
|
||||
new_entry
|
||||
.data
|
||||
.insert(name_member.get().member_name, ValueType::String(String::from("Testing")));
|
||||
new_entry
|
||||
.data
|
||||
.insert(info_member.get().member_name, ValueType::String(new_entry.id.to_string()));
|
||||
new_entry.data.insert(
|
||||
name_member.get().member_name,
|
||||
ValueType::String(String::from("Testing")),
|
||||
);
|
||||
new_entry.data.insert(
|
||||
info_member.get().member_name,
|
||||
ValueType::String(new_entry.id.to_string()),
|
||||
);
|
||||
entries.update(|entry_vec| entry_vec.push(new_entry));
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// A custom-styled select input.
|
||||
///
|
||||
/// Takes a `Vec<String>` as the set of inputs. Only one value may be selected, which is indicated
|
||||
/// by the `value` prop of the component.
|
||||
#[component]
|
||||
pub fn Select(
|
||||
#[prop(default = vec!["Test Value".into()])] value_list: Vec<String>,
|
||||
#[prop(default = RwSignal::new("".into()))] value: RwSignal<String>,
|
||||
#[prop(default = RwSignal::new("".into()))] label: RwSignal<String>,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div>
|
||||
<label>
|
||||
{label}
|
||||
<select
|
||||
class="relative border-2 m-1.5 p-1.5 rounded-md"
|
||||
class=(["border-red-700", "bg-transparent"], move || !disabled.get())
|
||||
class=(["border-gray-600", "pointer-events-none", "bg-gray-600/33"], move || disabled.get())
|
||||
on:change:target=move |e| {
|
||||
value.set(e.target().value());
|
||||
}>
|
||||
// This closure handles the display of the options.
|
||||
{move || {
|
||||
value_list.iter().map(
|
||||
move |value| {
|
||||
let display = value.to_owned();
|
||||
view! { <option value={display}>{display.clone()}</option> }
|
||||
}
|
||||
).collect::<Vec<_>>()
|
||||
}}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,39 @@
|
||||
use std::str::FromStr;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn OutlinedTextField(
|
||||
pub fn OutlinedTextField<T>(
|
||||
#[prop(default = String::from(""))] placeholder: String,
|
||||
#[prop(default = RwSignal::new("".to_string()))] value: RwSignal<String>,
|
||||
#[prop(default = RwSignal::new(T::default()))] value: RwSignal<T>,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[prop(default = String::from(""))] name: String,
|
||||
#[prop(default = String::from(""))] label: String,
|
||||
#[prop(default = String::from(""))] input_type: String,
|
||||
) -> impl IntoView {
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: Default + 'static + FromStr + ToString + PartialEq + Send + Sync + Clone
|
||||
{
|
||||
// Parsing logic for non-string types which happens on each input.
|
||||
let on_input = move |e| {
|
||||
let input_value = event_target_value(&e);
|
||||
let parsed_value = T::from_str(&input_value)
|
||||
.unwrap_or_default();
|
||||
value.set(parsed_value);
|
||||
};
|
||||
|
||||
view! (
|
||||
<label>
|
||||
{ label }
|
||||
<input
|
||||
class="border-2 m-1.5 p-1.5 rounded-md bg-transparent mt-6 relative transition-all duration-400"
|
||||
class=(["border-red-700"], move || !disabled.get())
|
||||
class=(["border-gray-600", "pointer-events-none"], move || disabled.get())
|
||||
class=(["border-red-700", "bg-transparent"], move || !disabled.get())
|
||||
class=(["border-gray-600", "pointer-events-none", "bg-gray-600/33"], move || disabled.get())
|
||||
r#type={input_type}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
prop:name=name
|
||||
bind:value=value // This is a two-way binding - it is allowed to read *and* write.
|
||||
prop:value={move || value.get().to_string()} // Sets the initial value.
|
||||
on:input=on_input // Sets the value on each input.
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
use leptos::prelude::event_target_value;
|
||||
|
||||
pub mod app;
|
||||
mod common;
|
||||
mod components;
|
||||
mod pages;
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
|
||||
+125
-68
@@ -1,38 +1,54 @@
|
||||
use crate::components::{ActionButton, OutlinedTextField};
|
||||
use crate::common::StudentInfo;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Client};
|
||||
use crate::app::Unauthenticated;
|
||||
use crate::common::{StudentInfo, StudentInfoReactive, UserClaims};
|
||||
use crate::components::{ActionButton, Loading, OutlinedTextField, Select};
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
||||
|
||||
#[server(GetSubmission, endpoint = "/get-submission")]
|
||||
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||
let dbclient = Client::new(&config);
|
||||
|
||||
console_log(format!("Getting values from API using key {}", id).as_str());
|
||||
console_log(format!("Getting values from API using username {}", id).as_str());
|
||||
|
||||
match dbclient
|
||||
.get_item()
|
||||
.table_name("student-applications")
|
||||
.key("Email", AttributeValue::S(id))
|
||||
.key("Email", AttributeValue::S(id.clone()))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
console_log("Found submission with passed key.");
|
||||
let item = output.item.unwrap();
|
||||
let first_name = item
|
||||
|
||||
let first_name: String = item
|
||||
.get("studentFirstName")
|
||||
.unwrap()
|
||||
.as_s()
|
||||
.unwrap()
|
||||
.clone();
|
||||
let last_name = item.get("studentLastName").unwrap().as_s().unwrap().clone();
|
||||
.and_then(|attr| attr.as_s().ok())
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let last_name: String = item
|
||||
.get("studentLastName")
|
||||
.and_then(|attr| attr.as_s().ok())
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
let math_sat_score = item
|
||||
.get("mathSAT")
|
||||
.and_then(|attr| attr.as_n().ok())
|
||||
.map(|n| n.parse::<i32>().unwrap_or(0))
|
||||
.unwrap_or_default();
|
||||
|
||||
console_log(format!("Got values from API: {} {}", first_name, last_name).as_str());
|
||||
|
||||
Ok(StudentInfo {
|
||||
first_name,
|
||||
last_name,
|
||||
math_sat_score,
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -43,7 +59,10 @@ pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
||||
}
|
||||
|
||||
#[server(CreateSampleSubmission, endpoint = "/create-sample-submission")]
|
||||
pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), ServerFnError> {
|
||||
pub async fn create_sample_submission(
|
||||
student_info: StudentInfo,
|
||||
subject: String,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use aws_sdk_dynamodb::{types::AttributeValue, Client};
|
||||
|
||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||
@@ -51,18 +70,16 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S
|
||||
|
||||
console_log(
|
||||
format!(
|
||||
"Creating sample submission with name {} {}",
|
||||
student_info.first_name, student_info.last_name
|
||||
"Creating sample submission with name {} {}, math score {}, and subject {}",
|
||||
student_info.first_name, student_info.last_name, student_info.math_sat_score, subject
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
let submission_id = String::from("testing_student");
|
||||
|
||||
match dbclient
|
||||
.update_item()
|
||||
.table_name("student-applications")
|
||||
.key("Email", AttributeValue::S(submission_id))
|
||||
.key("Email", AttributeValue::S(subject))
|
||||
.expression_attribute_values(
|
||||
":studentFirstName",
|
||||
AttributeValue::S(student_info.first_name),
|
||||
@@ -71,10 +88,17 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S
|
||||
":studentLastName",
|
||||
AttributeValue::S(student_info.last_name),
|
||||
)
|
||||
.expression_attribute_values(
|
||||
":mathScoreSAT",
|
||||
AttributeValue::N(student_info.math_sat_score.to_string()),
|
||||
)
|
||||
.expression_attribute_names("#studentFirstName", "studentFirstName")
|
||||
.expression_attribute_names("#studentLastName", "studentLastName")
|
||||
.expression_attribute_names("#mathScoreSAT", "mathScoreSAT")
|
||||
.update_expression(
|
||||
"SET #studentFirstName = :studentFirstName, #studentLastName = :studentLastName",
|
||||
concat!("SET #studentFirstName = :studentFirstName, ",
|
||||
"#studentLastName = :studentLastName, ",
|
||||
"#mathScoreSAT = :mathScoreSAT"),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
@@ -86,65 +110,98 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S
|
||||
|
||||
#[component]
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
let sample_id = RwSignal::new("google_113247439743075864879".to_string());
|
||||
// Creates a reactive value to update the button
|
||||
let count = RwSignal::new(0);
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
|
||||
let auth = use_context::<AuthSignal>().expect("Couldn't find user information.");
|
||||
let user_claims = Signal::derive(move || {
|
||||
auth.with(|auth| {
|
||||
auth.authenticated().and_then(|data| {
|
||||
data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Note that the value passed in MUST be equatable.
|
||||
// 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(
|
||||
move || sample_id.get(),
|
||||
async |id| {
|
||||
get_submission(id).await.unwrap_or_else(|e| {
|
||||
move || user_claims.get().map(|claim| claim.claims.subject.clone()),
|
||||
async |opt_username| match opt_username {
|
||||
Some(username) => get_submission(username).await.unwrap_or_else(|e| {
|
||||
console_log(e.to_string().as_str());
|
||||
StudentInfo {
|
||||
first_name: String::from("Error"),
|
||||
last_name: String::from(""),
|
||||
math_sat_score: 0,
|
||||
}
|
||||
})
|
||||
}),
|
||||
None => StudentInfo {
|
||||
first_name: String::new(),
|
||||
last_name: String::new(),
|
||||
math_sat_score: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
let on_click = move |_| {
|
||||
*count.write() += 1;
|
||||
*elements_disabled.write() = count.get() == 10;
|
||||
};
|
||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||
|
||||
view! {
|
||||
<h1>"Welcome to Leptos!"</h1>
|
||||
<p>
|
||||
"Lillian's reported full name from the API: "
|
||||
<Suspense fallback=move || view! { <span>"Loading..."</span> }>
|
||||
{move || {
|
||||
server_resource.get().map(|submission| {
|
||||
view! { <span>{submission.first_name}" "{submission.last_name}</span> }
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</p>
|
||||
<ActionForm action=submit_action>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
label="First Name".into()
|
||||
placeholder="John".into()
|
||||
disabled={elements_disabled}
|
||||
name="student_info[first_name]".into() />
|
||||
</div>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
label="Last Name".into()
|
||||
placeholder="Smith".into()
|
||||
disabled={elements_disabled}
|
||||
name="student_info[last_name]".into() />
|
||||
</div>
|
||||
<div>
|
||||
<ActionButton
|
||||
on:click=on_click
|
||||
disabled={elements_disabled}
|
||||
button_type="submit".into()
|
||||
>
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</div>
|
||||
</ActionForm>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=Unauthenticated>
|
||||
// Replace this fallback with a real loading screen.
|
||||
<Suspense fallback=Loading>
|
||||
{move || {
|
||||
server_resource.get().map(|submission: StudentInfo| {
|
||||
let reactive_info = StudentInfoReactive::new(submission);
|
||||
|
||||
let select_value = RwSignal::new(String::from("Math"));
|
||||
|
||||
view! {
|
||||
<p>
|
||||
"Current user's reported full name from the API: "
|
||||
{reactive_info.first_name}" "{reactive_info.last_name}
|
||||
</p>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
label="First Name".into()
|
||||
placeholder="John".into()
|
||||
value=reactive_info.first_name />
|
||||
</div>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
label="Last Name".into()
|
||||
placeholder="Smith".into()
|
||||
value=reactive_info.last_name />
|
||||
</div>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
label="Testing".into()
|
||||
placeholder="Test".into()
|
||||
value=reactive_info.math_sat_score />
|
||||
</div>
|
||||
<Select
|
||||
value_list = vec!["Math".into(), "English".into(), "Science".into()]
|
||||
value = select_value
|
||||
/>
|
||||
<div>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
submit_action.dispatch(CreateSampleSubmission {
|
||||
student_info: reactive_info.capture(),
|
||||
subject: user_claims.get().unwrap().claims.subject.clone()
|
||||
});
|
||||
}
|
||||
>
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
//! This defines the base component of the Chromebook loaner kiosk application.
|
||||
//! This defines the base component of the Chromebook loaner kiosk application.
|
||||
//! There are going to be 3 definitive screens that the users are able to see and use:
|
||||
//! - The username entry screen: users will enter their name and email.
|
||||
//! - The action selection screen: users will be given a choice regarding their loaner status.
|
||||
//! - The completion screen: users will be given a success message should their information save.
|
||||
//! Refer to each component's file for more information about how they work.
|
||||
|
||||
use leptos::prelude::*;
|
||||
use super::{action_selection::ActionSelection, user_entry::UserEntry};
|
||||
use crate::common::LoanerUser;
|
||||
use super::{
|
||||
user_entry::UserEntry,
|
||||
action_selection::ActionSelection
|
||||
};
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// Used to keep track of the currently displayed controls.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum LoanerKioskState {
|
||||
UserEntry,
|
||||
ActionSelection,
|
||||
Complete
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// The base Chromebook loaner kiosk component.
|
||||
@@ -27,7 +24,7 @@ pub enum LoanerKioskState {
|
||||
pub fn LoanerKiosk() -> impl IntoView {
|
||||
let current_state = RwSignal::new(LoanerKioskState::UserEntry);
|
||||
let user = RwSignal::new(LoanerUser::new());
|
||||
|
||||
|
||||
view! {
|
||||
<div class="flex w-screen h-screen justify-center items-center">
|
||||
<UserEntry
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
mod loaner_kiosk;
|
||||
mod user_entry;
|
||||
mod action_selection;
|
||||
|
||||
mod loaner_kiosk;
|
||||
mod user_entry;
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Contains the user entry component.
|
||||
//! Contains the user entry component.
|
||||
//! It would be preferred if the user was restricted to a real user account. This is the URL:
|
||||
//! https://admin.googleapis.com/admin/directory/v1/users?domain=region15.org&projection=BASIC&query=isArchived=false%20isAdmin=false
|
||||
//! Once they select this, they can then check in/out the loaners under their account.
|
||||
@@ -6,10 +6,10 @@
|
||||
//! a service account. That's going to be a pain to set up.
|
||||
//! For example's sake here, we're just going to take a first name, last name, and email address.
|
||||
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use crate::common::LoanerUser;
|
||||
use crate::components::{ActionButton, OutlinedTextField};
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
|
||||
/// The user entry component for the loaner kiosk.
|
||||
///
|
||||
@@ -20,11 +20,14 @@ use crate::components::{ActionButton, OutlinedTextField};
|
||||
#[component]
|
||||
pub fn UserEntry(
|
||||
/// A callback function used when the control has determined this stage is finished.
|
||||
#[prop()] mut on_submit: impl FnMut() + 'static,
|
||||
#[prop()]
|
||||
mut on_submit: impl FnMut() + 'static,
|
||||
/// The global user record.
|
||||
#[prop()] user: RwSignal<LoanerUser>,
|
||||
#[prop()]
|
||||
user: RwSignal<LoanerUser>,
|
||||
/// Whether this control is shown. Required for use with CSS animations.
|
||||
#[prop()] shown: bool
|
||||
#[prop()]
|
||||
shown: bool,
|
||||
) -> impl IntoView {
|
||||
let disabled = RwSignal::new(false);
|
||||
let on_click = move |_| {
|
||||
@@ -44,16 +47,19 @@ pub fn UserEntry(
|
||||
label="First Name".into()
|
||||
placeholder="John".into()
|
||||
disabled={disabled}
|
||||
value=RwSignal::new("".to_owned())
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name".into()
|
||||
placeholder="Smith".into()
|
||||
disabled={disabled}
|
||||
value=RwSignal::new("".to_owned())
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Region 15 Email".into()
|
||||
placeholder="jsmith@region15.org".into()
|
||||
disabled={disabled}
|
||||
value=RwSignal::new("".to_owned())
|
||||
/>
|
||||
<ActionButton
|
||||
on:click=on_click
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
mod about_page;
|
||||
mod home_page;
|
||||
mod provider_entry;
|
||||
mod loaner_kiosk;
|
||||
mod provider_entry;
|
||||
|
||||
pub use self::{about_page::*, home_page::*, provider_entry::*};
|
||||
+38
-19
@@ -1,27 +1,46 @@
|
||||
use crate::components::{MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField};
|
||||
use crate::app::Unauthenticated;
|
||||
use crate::common::UserClaims;
|
||||
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField};
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[component]
|
||||
pub fn ProviderEntry() -> impl IntoView {
|
||||
let auth = use_context::<AuthSignal>().expect("Couldn't find auth signal");
|
||||
let auth_data = auth
|
||||
.get()
|
||||
.authenticated()
|
||||
.and_then(|data| data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]));
|
||||
|
||||
let multi_entries: RwSignal<Vec<MultiEntryData>> = RwSignal::new(vec![]);
|
||||
|
||||
|
||||
console_log(format!("User claims: {auth_data:?}").as_str());
|
||||
|
||||
view! (
|
||||
<h1>This is the provider entry page</h1>
|
||||
<h2>It would usually have a series of questions that we ask the providers</h2>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
placeholder = "Testing information...".into()
|
||||
name = "testing_input".into()
|
||||
label = "This is a testing question.".into()
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>This is an example of the multientry element.</p>
|
||||
<MultiEntry
|
||||
entries=multi_entries
|
||||
name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name"))
|
||||
info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info"))
|
||||
/>
|
||||
</div>
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=Unauthenticated>
|
||||
<LogoutLink class="text-logout">"Sign out"</LogoutLink>
|
||||
<h1>This is the provider entry page</h1>
|
||||
<h2>It would usually have a series of questions that we ask the providers</h2>
|
||||
<div>
|
||||
<OutlinedTextField
|
||||
placeholder = "Testing information...".into()
|
||||
name = "testing_input".into()
|
||||
label = "This is a testing question.".into()
|
||||
value = RwSignal::new("".to_owned())
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>This is an example of the multientry element.</p>
|
||||
<MultiEntry
|
||||
entries=multi_entries
|
||||
name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name"))
|
||||
info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info"))
|
||||
/>
|
||||
</div>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user