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 /src | |
| parent | 31193f937cf9a92eb314f7040bfeac109f683cc1 (diff) | |
| download | wasm-card-104f70b0968d7138d6cf944da98d95a405b1a049.tar.gz wasm-card-104f70b0968d7138d6cf944da98d95a405b1a049.tar.bz2 | |
add addresses, generate name, improve responsiveness
Diffstat (limited to 'src')
| -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 | 
