diff options
author | jelemux <jeremias.weber@protonmail.com> | 2020-11-11 23:28:29 +0100 |
---|---|---|
committer | jelemux <jeremias.weber@protonmail.com> | 2020-11-11 23:28:29 +0100 |
commit | 104f70b0968d7138d6cf944da98d95a405b1a049 (patch) | |
tree | c4bade76c3cbe5b39348334dee952c4cf955c827 | |
parent | 31193f937cf9a92eb314f7040bfeac109f683cc1 (diff) | |
download | wasm-card-104f70b0968d7138d6cf944da98d95a405b1a049.tar.gz wasm-card-104f70b0968d7138d6cf944da98d95a405b1a049.tar.bz2 |
add addresses, generate name, improve responsiveness
-rw-r--r-- | src/address.rs | 239 | ||||
-rw-r--r-- | src/lib.rs | 221 | ||||
-rw-r--r-- | src/name.rs | 44 |
3 files changed, 404 insertions, 100 deletions
diff --git a/src/address.rs b/src/address.rs new file mode 100644 index 0000000..d048dc0 --- /dev/null +++ b/src/address.rs @@ -0,0 +1,239 @@ +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; +use std::collections::HashSet; + +#[derive(Clone)] +pub struct Address { + pub post_office_box: String, + pub extension: String, + pub street: String, + pub locality: String, + pub region: String, + pub code: String, + pub country: String, + address_type: AddressType, +} + +impl Address { + pub fn new_with_type(address_type: AddressType) -> Self { + Self { + post_office_box: String::new(), + extension: String::new(), + street: String::new(), + locality: String::new(), + region: String::new(), + code: String::new(), + country: String::new(), + address_type + } + } + pub fn to_vcard_address(&self) -> properties::Address { + let address_value = values::address_value::AddressValue::from_components( + match self.post_office_box.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.post_office_box).unwrap()), + }, + match self.extension.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.extension).unwrap()), + }, + match self.street.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.street).unwrap()), + }, + match self.locality.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.locality).unwrap()), + }, + match self.region.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.region).unwrap()), + }, + match self.code.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.code).unwrap()), + }, + match self.country.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.country).unwrap()), + }, + ); + + let mut address = properties::Address::from_address_value(address_value); + + let type_values = { + let mut type_values = HashSet::new(); + + type_values.insert( + match self.address_type { + AddressType::Home => values::type_value::TypeValue::Home, + AddressType::Work => values::type_value::TypeValue::Work, + } + ); + + vcard::Set::from_hash_set(type_values).unwrap() + }; + + address.typ = Some(parameters::typ::Type::from_type_values(type_values)); + + address + } +} + +#[derive(Clone, Copy, PartialEq)] +pub enum AddressType { + Home, + Work, +} + +impl AddressType { + pub fn to_str(&self) -> &str { + match self { + AddressType::Home => "Home", + AddressType::Work => "Work", + } + } +} + +pub struct AddressView { + link: ComponentLink<Self>, + value: Address, + oninput: Callback<Address>, +} + +pub enum Msg { + UpdatePostOfficeBox(String), + UpdateExtension(String), + UpdateStreet(String), + UpdateLocality(String), + UpdateRegion(String), + UpdateCode(String), + UpdateCountry(String), +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + pub oninput: Callback<Address>, + pub address_type: AddressType, + //pub errors: Vec<String>, +} + +impl Component for AddressView { + type Message = Msg; + type Properties = Props; + fn create(props: <Self as yew::Component>::Properties, link: yew::html::Scope<Self>) -> Self { + Self { + link, + value: Address::new_with_type(props.address_type), + oninput: props.oninput, + } + } + fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { + match msg { + Msg::UpdatePostOfficeBox(b) => self.value.post_office_box = b, + Msg::UpdateExtension(e) => self.value.extension = e, + Msg::UpdateStreet(s) => self.value.street = s, + Msg::UpdateLocality(l) => self.value.locality = l, + Msg::UpdateRegion(r) => self.value.region = r, + Msg::UpdateCode(p) => self.value.code = p, + Msg::UpdateCountry(c) => self.value.country = c, + }; + self.oninput.emit(self.value.clone()); + true + } + fn change(&mut self, props: <Self as yew::Component>::Properties) -> bool { + self.oninput = props.oninput; + self.value.address_type = props.address_type; + true + } + fn view(&self) -> yew::virtual_dom::VNode { + html!{ + <div class="box"> + <h3 class="subtitle">{ format!("{} Address", self.value.address_type.to_str()) }</h3> + + <div class="columns is-mobile is-multiline"> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Post Office Box" }</label> + <div class="control"> + <input id="post_office_box" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdatePostOfficeBox(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Extension" }</label> + <div class="control"> + <input id="extension" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Street" }</label> + <div class="control"> + <input id="street" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateStreet(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Locality" }</label> + <div class="control"> + <input id="locality" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateLocality(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Region" }</label> + <div class="control"> + <input id="region" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateRegion(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Postal Code" }</label> + <div class="control"> + <input id="code" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateCode(e.value)) + /> + </div> + </div> + + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> + <label class="label">{ "Country" }</label> + <div class="control"> + <input id="country" + type="text" + placeholder="" + oninput=self.link.callback(|e: InputData| Msg::UpdateCountry(e.value)) + /> + </div> + </div> + + </div> + </div> + } + } +}
\ No newline at end of file @@ -1,7 +1,9 @@ #![recursion_limit="1024"] extern crate wee_alloc; extern crate console_error_panic_hook; +use std::collections::HashSet; use name::{NameView,Name}; +use address::{AddressView,Address,AddressType}; use genpdf::Element as _; use genpdf::{elements, style, fonts}; use qrcodegen::QrCode; @@ -12,6 +14,7 @@ use vcard::{VCard, VCardError}; use std::panic; mod name; +mod address; // Use `wee_alloc` as the global allocator. #[global_allocator] @@ -21,6 +24,7 @@ fn init() { panic::set_hook(Box::new(console_error_panic_hook::hook)); } +#[derive(Clone)] pub struct Download { pub file_name: String, pub content: String, @@ -36,16 +40,7 @@ impl Download { } } -impl Clone for Download { - fn clone(&self) -> Self { - Self { - file_name: self.file_name.clone(), - content: self.content.clone(), - mime_type: self.mime_type.clone(), - } - } -} - +#[derive(Clone, Copy)] pub enum MimeType { PDF, VCard, @@ -62,28 +57,28 @@ impl MimeType { } } -impl Clone for MimeType { - fn clone(&self) -> Self { - match self { - MimeType::PDF => MimeType::PDF, - MimeType::VCard => MimeType::VCard, - MimeType::SVG => MimeType::SVG, - } - } +#[derive(Clone, Copy)] +pub enum DownloadOption { + PDF, + VCard, + QrCode, } pub struct MainView { link: ComponentLink<Self>, error: Vec<String>, name: Name, + work_address: Address, + home_address: Address, download: Option<Download>, + selected_option: DownloadOption, } pub enum Msg { UpdateName(Name), - GenerateVCard, - GeneratePdf, - GenerateQrCode, + UpdateHomeAddress(Address), + UpdateWorkAddress(Address), + Generate(DownloadOption), Nope, } @@ -92,56 +87,85 @@ impl Component for MainView { type Properties = (); fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { - MainView { link, error: vec![], name: Name::new(), download: None, } + MainView { + link, + error: vec![], + name: Name::new(), + work_address: Address::new_with_type(AddressType::Work), + home_address: Address::new_with_type(AddressType::Home), + download: None, + selected_option: DownloadOption::VCard + } } fn update(&mut self, msg: Self::Message) -> ShouldRender { self.error.clear(); match msg { - Msg::UpdateName(value) => self.name = value, - Msg::GenerateVCard => { - match self.generate_vcard() { - Ok(vcard) => self.download = Some( - Download { - file_name: format!("{}.vcs", self.name.formatted_name()), - content: vcard.to_string(), - mime_type: MimeType::VCard, - } - ), - Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), - Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), + Msg::UpdateName(value) => { + self.name = value; + self.link.send_message(Msg::Generate(self.selected_option)); + }, + Msg::UpdateHomeAddress(value) => { + self.home_address = value; + self.link.send_message(Msg::Generate(self.selected_option)); + }, + Msg::UpdateWorkAddress(value) => { + self.work_address = value; + self.link.send_message(Msg::Generate(self.selected_option)); + }, + Msg::Generate(option) => { + self.selected_option = option; + + let vcard_content = match self.generate_vcard() { + Ok(vcard) => Some(vcard.to_string()), + Err(VCardError::FormatError(err)) => { + self.error.push(err.to_string()); + None + } + Err(VCardError::EmptyFormatName) => { + self.error.push(String::from("At least one of the name fields should be filled out.")); + None + } }; - } - Msg::GeneratePdf => { - match self.generate_pdf() { - Ok(pdf) => self.download = Some( - Download { - file_name: format!("Visitenkarten {}.pdf", self.name.formatted_name()), - content: pdf, - mime_type: MimeType::PDF, + + match option { + DownloadOption::VCard => { + if vcard_content.is_some() { + self.download = Some( + Download { + file_name: format!("{}.vcs", self.name.formatted_name()), + content: vcard_content.unwrap().to_string(), + mime_type: MimeType::VCard, + } + ) } - ), - Err(_) => self.error.push(String::from("Unexpected error while generating the PDF. Please contact me about it.")), - } - } - Msg::GenerateQrCode => { - let mut vcard_content = None; - match self.generate_vcard() { - Ok(vcard) => vcard_content = Some(vcard.to_string()), - Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), - Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), - }; - if vcard_content.is_some() { - match QrCode::encode_text(vcard_content.as_ref().unwrap(), QrCodeEcc::Low) { - Ok(qr) => self.download = Some( - Download { - file_name: format!("QR-Code VCard {}.svg", self.name.formatted_name()), - content: qr.to_svg_string(4), - mime_type: MimeType::SVG, - } - ), - Err(_) => self.error.push(String::from("Sorry, VCard is too long!")), - }; + } + DownloadOption::QrCode => { + if vcard_content.is_some() { + match QrCode::encode_text(vcard_content.as_ref().unwrap(), QrCodeEcc::Low) { + Ok(qr) => self.download = Some( + Download { + file_name: format!("QR-Code VCard {}.svg", self.name.formatted_name()), + content: qr.to_svg_string(4), + mime_type: MimeType::SVG, + } + ), + Err(_) => self.error.push(String::from("Sorry, VCard is too long!")), + }; + } + } + DownloadOption::PDF => { + match self.generate_pdf() { + Ok(pdf) => self.download = Some( + Download { + file_name: format!("Visitenkarten {}.pdf", self.name.formatted_name()), + content: pdf, + mime_type: MimeType::PDF, + } + ), + Err(_) => self.error.push(String::from("Unexpected error while generating the PDF. Please contact me about it.")), + } + } } } Msg::Nope => return false, @@ -158,29 +182,12 @@ impl Component for MainView { fn view(&self) -> Html { - let download = self.download.clone(); - - let on_name_input = self.link.batch_callback(move |n: Name| - if download.is_some() { - vec![ - Msg::UpdateName(n), - match download.as_ref().unwrap().mime_type { - MimeType::PDF => Msg::GeneratePdf, - MimeType::SVG => Msg::GenerateQrCode, - MimeType::VCard => Msg::GenerateVCard, - } - ] - } else { - vec![Msg::UpdateName(n), Msg::GenerateVCard] - } - ); - let download_options = self.link.callback(|e: ChangeData| match e { ChangeData::Select(v) => match v.value().as_str() { - "vcard" => Msg::GenerateVCard, - "pdf" => Msg::GeneratePdf, - "qrcode" => Msg::GenerateQrCode, + "vcard" => Msg::Generate(DownloadOption::VCard), + "pdf" => Msg::Generate(DownloadOption::PDF), + "qrcode" => Msg::Generate(DownloadOption::QrCode), _ => Msg::Nope, }, _ => Msg::Nope, @@ -204,10 +211,14 @@ impl Component for MainView { { self.render_errors() } - <NameView oninput=on_name_input /> + <NameView oninput=self.link.callback(|n: Name| Msg::UpdateName(n)) /> - <div class="block"> - <div class="select"> + <AddressView address_type=AddressType::Home oninput=self.link.callback(|a: Address| Msg::UpdateHomeAddress(a)) /> + + <AddressView address_type=AddressType::Work oninput=self.link.callback(|a: Address| Msg::UpdateWorkAddress(a)) /> + + <div class="block level-left"> + <div class="select level-item"> <select id="download_options" onchange=download_options> <option value="vcard">{ "VCard (.vcf)" }</option> <option value="pdf">{ "Print-ready PDF" }</option> @@ -260,7 +271,7 @@ impl MainView { let download = self.download.as_ref().unwrap(); html!{ - <a href=download.as_data_link() download=download.file_name class="button is-primary" > + <a href=download.as_data_link() download=download.file_name class="button is-primary level-item" > { "Download" } </a> } @@ -276,9 +287,13 @@ impl MainView { MimeType::PDF => html!{ <iframe src=download.as_data_link() alt="PDF Preview" width="400" height="550"/> }, - MimeType::VCard => html!{ - <code> { download.content.clone() } </code> - }, + MimeType::VCard => { + html!{ + <pre> + <code> { download.content.clone() } </code> + </pre> + } + } MimeType::SVG => html!{ <img src=download.as_data_link() alt="Image Preview" class="image is-square" width="300" height="300"/> }, @@ -289,7 +304,29 @@ impl MainView { } fn generate_vcard(&self) -> Result<VCard, VCardError> { match VCard::from_formatted_name_str(&self.name.formatted_name()) { - Ok(vcard) => Ok(vcard), + Ok(vcard) => { + let mut vcard = vcard; + + let names = { + let mut names = HashSet::new(); + names.insert(self.name.to_vcard_name()); + + names + }; + + let addresses = { + let mut addresses = HashSet::new(); + addresses.insert(self.home_address.to_vcard_address()); + addresses.insert(self.work_address.to_vcard_address()); + + addresses + }; + + vcard.names = Some(vcard::Set::from_hash_set(names).unwrap()); + vcard.addresses = Some(vcard::Set::from_hash_set(addresses).unwrap()); + + Ok(vcard) + } Err(err) => Err(err), } } diff --git a/src/name.rs b/src/name.rs index bf91186..4b51def 100644 --- a/src/name.rs +++ b/src/name.rs @@ -1,4 +1,6 @@ use yew::prelude::*; +use vcard::properties; +use vcard::values::{self, text}; #[derive(Clone)] pub struct Name { @@ -44,6 +46,32 @@ impl Name { formatted_name } + pub fn to_vcard_name(&self) -> properties::Name { + let name_value = values::name_value::NameValue::from_components( + match self.last_name.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.last_name).unwrap()), + }, + match self.first_name.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.first_name).unwrap()), + }, + match self.middle_name.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.middle_name).unwrap()), + }, + match self.prefix.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.prefix).unwrap()), + }, + match self.suffix.is_empty() { + true => None, + false => Some(text::Component::from_str(&self.suffix).unwrap()), + }, + ); + + properties::Name::from_name_value(name_value) + } } pub struct NameView { @@ -94,12 +122,12 @@ impl Component for NameView { } fn view(&self) -> yew::virtual_dom::VNode { html!{ - <> + <div class="box"> <h3 class="subtitle">{ "Name" }</h3> - <div class="columns"> + <div class="columns is-mobile is-multiline"> - <div class="field column"> + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <label class="label">{ "Prefix" }</label> <div class="control"> <input id="prefix" @@ -110,7 +138,7 @@ impl Component for NameView { </div> </div> - <div class="field column"> + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <label class="label">{ "First name" }</label> <div class="control"> <input id="first_name" @@ -121,7 +149,7 @@ impl Component for NameView { </div> </div> - <div class="field column"> + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <label class="label">{ "Middle name" }</label> <div class="control"> <input id="middle_name" @@ -132,7 +160,7 @@ impl Component for NameView { </div> </div> - <div class="field column"> + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <label class="label">{ "Last name" }</label> <div class="control"> <input id="last_name" @@ -143,7 +171,7 @@ impl Component for NameView { </div> </div> - <div class="field column"> + <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> <label class="label">{ "Suffix" }</label> <div class="control"> <input id="suffix" @@ -155,7 +183,7 @@ impl Component for NameView { </div> </div> - </> + </div> } } }
\ No newline at end of file |