diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | src/util.rs | 39 | ||||
-rw-r--r-- | src/view/address.rs | 181 | ||||
-rw-r--r-- | src/view/input_objects/address.rs | 151 | ||||
-rw-r--r-- | src/view/input_objects/mod.rs | 106 | ||||
-rw-r--r-- | src/view/input_objects/name.rs | 118 | ||||
-rw-r--r-- | src/view/input_objects/telephone.rs | 162 | ||||
-rw-r--r-- | src/view/input_objects/utility.rs | 39 | ||||
-rw-r--r-- | src/view/main.rs | 335 | ||||
-rw-r--r-- | src/view/mod.rs | 388 | ||||
-rw-r--r-- | src/view/name.rs | 138 | ||||
-rw-r--r-- | src/view/telephone.rs | 137 |
12 files changed, 1007 insertions, 790 deletions
@@ -4,14 +4,13 @@ extern crate console_error_panic_hook; use wasm_bindgen::prelude::*; use yew::prelude::App; use std::panic; -use view::MainView; +use view::main::MainView; // Use `wee_alloc` as the global allocator. #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; mod view; -mod util; fn init() { panic::set_hook(Box::new(console_error_panic_hook::hook)); diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 3d8f231..0000000 --- a/src/util.rs +++ /dev/null @@ -1,39 +0,0 @@ -use yew::prelude::*; - -pub fn text_field_input(label: &str, id: &str, placeholder: Option<&str>, oninput: Callback<InputData>) -> Html { - html!{ - <div class="field column - is-one-fifth-widescreen - is-one-quarter-desktop - is-one-third-tablet - is-half-mobile" > - <label class="label">{ label }</label> - <div class="control"> - <input id=id - type="text" - placeholder=placeholder.unwrap_or("") - oninput=oninput - /> - </div> - </div> - } -} - -pub fn checkbox_field_input(label: &str, id: &str, checked: bool, onclick: Callback<MouseEvent>) -> Html { - html!{ - <div class="field column - is-one-fifth-widescreen - is-one-quarter-desktop - is-one-third-tablet - is-half-mobile" > - <label class="checkbox"> - <input id=id - type="checkbox" - checked=checked - onclick=onclick - /> - { label } - </label> - </div> - } -}
\ No newline at end of file diff --git a/src/view/address.rs b/src/view/address.rs index a30ba85..4d3eae7 100644 --- a/src/view/address.rs +++ b/src/view/address.rs @@ -1,108 +1,14 @@ use yew::prelude::*; use vcard::properties; -use vcard::parameters; -use vcard::values::{self, text}; -use std::collections::HashSet; - -use crate::util; - -#[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_value(&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", - } - } -} +use super::input_objects::address::*; +use super::input_objects::VCardPropertyInputObject; +use super::VCardPropertyInputComponent; pub struct AddressView { link: ComponentLink<Self>, value: Address, oninput: Callback<Address>, + errors: Vec<String>, } pub enum Msg { @@ -113,23 +19,37 @@ pub enum Msg { UpdateRegion(String), UpdateCode(String), UpdateCountry(String), + ToggleWork, + ToggleHome, } #[derive(Clone, PartialEq, Properties)] pub struct Props { pub oninput: Callback<Address>, - pub address_type: AddressType, //pub errors: Vec<String>, } +impl VCardPropertyInputComponent<properties::Address, Address> for AddressView { + fn get_input_object(&self) -> Address { + self.value.clone() + } + fn get_title(&self) -> String { + "Address".to_string() + } + fn get_errors(&self) -> Vec<String> { + self.errors.clone() + } +} + 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), + value: Address::new(), oninput: props.oninput, + errors: vec![], } } fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { @@ -141,72 +61,29 @@ impl Component for AddressView { Msg::UpdateRegion(r) => self.value.region = r, Msg::UpdateCode(p) => self.value.code = p, Msg::UpdateCountry(c) => self.value.country = c, + Msg::ToggleWork => self.value.work = !self.value.work, + Msg::ToggleHome => self.value.home = !self.value.home, }; 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"> - - { util::text_field_input( - "Post Office Box", - "post_office_box", - None, - self.link.callback(|e: InputData| Msg::UpdatePostOfficeBox(e.value)) - ) } - - { util::text_field_input( - "Extension", - "extension", - None, - self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) - ) } - - { util::text_field_input( - "Street", - "street", - None, - self.link.callback(|e: InputData| Msg::UpdateStreet(e.value)) - ) } - - { util::text_field_input( - "Locality", - "locality", - None, - self.link.callback(|e: InputData| Msg::UpdateLocality(e.value)) - ) } - - { util::text_field_input( - "Region", - "region", - None, - self.link.callback(|e: InputData| Msg::UpdateRegion(e.value)) - ) } + { + self.render_errors() + } - { util::text_field_input( - "Postal Code", - "code", - None, - self.link.callback(|e: InputData| Msg::UpdateCode(e.value)) - ) } + <h3 class="subtitle">{ self.get_title() }</h3> - { util::text_field_input( - "Country", - "country", - None, - self.link.callback(|e: InputData| Msg::UpdateCountry(e.value)) - ) } + { + self.get_input_object().render(&self.link) + } - </div> </div> } } diff --git a/src/view/input_objects/address.rs b/src/view/input_objects/address.rs new file mode 100644 index 0000000..3975200 --- /dev/null +++ b/src/view/input_objects/address.rs @@ -0,0 +1,151 @@ +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; +use std::collections::HashSet; +use super::*; +use super::super::address::*; + +#[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, + pub work: bool, + pub home: bool, +} + +impl VCardPropertyInputObject<properties::Address, AddressView> for Address { + fn new() -> 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(), + work: false, + home: false, + } + } + fn get_input_fields(&self, link: &ComponentLink<AddressView>) -> Vec<VCardPropertyInputField> { + vec![ + VCardPropertyInputField::Text{ + label: "Post Office Box".to_string(), + id: Some("post_office_box".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdatePostOfficeBox(e.value)), + value: self.post_office_box.clone(), + }, + VCardPropertyInputField::Text{ + label: "Extension".to_string(), + id: Some("extension".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateExtension(e.value)), + value: self.extension.clone(), + }, + VCardPropertyInputField::Text{ + label: "Street".to_string(), + id: Some("street".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateStreet(e.value)), + value: self.street.clone(), + }, + VCardPropertyInputField::Text{ + label: "Locality".to_string(), + id: Some("locality".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateLocality(e.value)), + value: self.locality.clone(), + }, + VCardPropertyInputField::Text{ + label: "Region".to_string(), + id: Some("region".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateRegion(e.value)), + value: self.region.clone(), + }, + VCardPropertyInputField::Text{ + label: "Code".to_string(), + id: Some("code".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateCode(e.value)), + value: self.code.clone(), + }, + VCardPropertyInputField::Text{ + label: "Country".to_string(), + id: Some("country".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateCountry(e.value)), + value: self.country.clone(), + }, + VCardPropertyInputField::CheckBox{ + label: "Work".to_string(), + id: Some("work".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleWork), + value: self.work, + }, + VCardPropertyInputField::CheckBox{ + label: "Home".to_string(), + id: Some("home".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleHome), + value: self.home, + }, + ] + } + fn to_vcard_property(&self) -> Result<properties::Address, VCardPropertyInputError> { + 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(); + + if self.work { + type_values.insert(values::type_value::TypeValue::Work); + } + if self.home { + type_values.insert(values::type_value::TypeValue::Home); + } + + vcard::Set::from_hash_set(type_values).unwrap() + }; + + address.typ = Some(parameters::typ::Type::from_type_values(type_values)); + + Ok(address) + } +}
\ No newline at end of file diff --git a/src/view/input_objects/mod.rs b/src/view/input_objects/mod.rs new file mode 100644 index 0000000..7c0fdff --- /dev/null +++ b/src/view/input_objects/mod.rs @@ -0,0 +1,106 @@ +use vcard::properties; +use yew::prelude::*; +use super::VCardPropertyInputComponent; + +pub mod address; +pub mod birthday; +pub mod name; +pub mod photo; +pub mod telephone; +pub mod utility; + +pub trait VCardPropertyInputObject<P: properties::Property, C: VCardPropertyInputComponent<P, Self>> + where Self: Sized +{ + fn new() -> Self; + fn get_input_fields(&self, link: &ComponentLink<C>) -> Vec<VCardPropertyInputField>; + fn render(&self, link: &ComponentLink<C>) -> Html { + html!{ + <div class="columns is-mobile is-multiline"> + { + for self.get_input_fields(link).iter().map(|field| + field.render() + ) + } + </div> + } + } + fn to_vcard_property(&self) -> Result<P, VCardPropertyInputError>; +} + +#[derive(Debug)] +pub struct VCardPropertyInputError { + msg: String, +} + +pub enum VCardPropertyInputField { + Text { + label: String, + id: Option<String>, + placeholder: Option<String>, + oninput: Callback<InputData>, + value: String, + }, + CheckBox { + label: String, + id: Option<String>, + onclick: Callback<MouseEvent>, + value: bool, + }, +} + +impl VCardPropertyInputField { + pub fn render(&self) -> Html { + match self { + Self::Text { + label, + id, + placeholder, + oninput, + value: _, + } => Self::text_field_input(label, id, placeholder, oninput), + Self::CheckBox { + label, + id, + onclick, + value, + } => Self::checkbox_field_input(label, id, value, onclick), + } + } + fn text_field_input(label: &str, id: &Option<String>, placeholder: &Option<String>, oninput: &Callback<InputData>) -> Html { + html!{ + <div class="field column + is-one-fifth-widescreen + is-one-quarter-desktop + is-one-third-tablet + is-half-mobile" > + <label class="label">{ label }</label> + <div class="control"> + <input id=id.as_ref().unwrap_or(&"".to_string()) + type="text" + placeholder=placeholder.as_ref().unwrap_or(&"".to_string()) + oninput=oninput + /> + </div> + </div> + } + } + fn checkbox_field_input(label: &str, id: &Option<String>, checked: &bool, onclick: &Callback<MouseEvent>) -> Html { + html!{ + <div class="field column + is-one-fifth-widescreen + is-one-quarter-desktop + is-one-third-tablet + is-half-mobile" > + <label class="checkbox"> + <input id=id.as_ref().unwrap_or(&"".to_string()) + type="checkbox" + checked=*checked + onclick=onclick + /> + { label } + </label> + </div> + } + } +}
\ No newline at end of file diff --git a/src/view/input_objects/name.rs b/src/view/input_objects/name.rs new file mode 100644 index 0000000..0fe9e64 --- /dev/null +++ b/src/view/input_objects/name.rs @@ -0,0 +1,118 @@ +use vcard::properties; +use vcard::values::{self, text}; +use super::*; +use super::super::name::*; + +#[derive(Clone)] +pub struct Name { + pub prefix: String, + pub first_name: String, + pub middle_name: String, + pub last_name: String, + pub suffix: String, +} + +impl VCardPropertyInputObject<properties::Name, NameView> for Name { + fn new() -> Self { + Self { + prefix: String::new(), + first_name: String::new(), + middle_name: String::new(), + last_name: String::new(), + suffix: String::new(), + } + } + fn get_input_fields(&self, link: &ComponentLink<NameView>) -> std::vec::Vec<VCardPropertyInputField> { + vec![ + VCardPropertyInputField::Text{ + label: "Prefix".to_string(), + id: Some("prefix".to_string()), + placeholder: Some("Sir".to_string()), + oninput: link.callback(|e: InputData| Msg::UpdatePrefix(e.value)), + value: self.prefix.clone(), + }, + VCardPropertyInputField::Text{ + label: "First Name".to_string(), + id: Some("first_name".to_string()), + placeholder: Some("Arthur".to_string()), + oninput: link.callback(|e: InputData| Msg::UpdateFirstName(e.value)), + value: self.first_name.clone(), + }, + VCardPropertyInputField::Text{ + label: "Middle Name".to_string(), + id: Some("middle_name".to_string()), + placeholder: Some("Charles".to_string()), + oninput: link.callback(|e: InputData| Msg::UpdateMiddleName(e.value)), + value: self.middle_name.clone(), + }, + VCardPropertyInputField::Text{ + label: "Last Name".to_string(), + id: Some("last_name".to_string()), + placeholder: Some("Clarke".to_string()), + oninput: link.callback(|e: InputData| Msg::UpdateLastName(e.value)), + value: self.last_name.clone(), + }, + VCardPropertyInputField::Text{ + label: "Suffix".to_string(), + id: Some("suffix".to_string()), + placeholder: Some("CBE FRAS".to_string()), + oninput: link.callback(|e: InputData| Msg::UpdateSuffix(e.value)), + value: self.suffix.clone(), + }, + ] + } + fn to_vcard_property(&self) -> std::result::Result<properties::Name, VCardPropertyInputError> { + 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()), + }, + ); + + Ok(properties::Name::from_name_value(name_value)) + } +} + +impl Name { + pub fn formatted_name(&self) -> String { + let mut formatted_name = String::new(); + + if !self.prefix.is_empty() { + formatted_name.push_str(&self.prefix); + } + if !self.first_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.first_name); + } + if !self.middle_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.middle_name); + } + if !self.last_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.last_name); + } + if !self.suffix.is_empty() { + formatted_name.push_str(", "); + formatted_name.push_str(&self.suffix); + } + + formatted_name + } +}
\ No newline at end of file diff --git a/src/view/input_objects/telephone.rs b/src/view/input_objects/telephone.rs new file mode 100644 index 0000000..d7f7194 --- /dev/null +++ b/src/view/input_objects/telephone.rs @@ -0,0 +1,162 @@ +use vcard::properties; +use vcard::parameters; +use vcard::values; +use std::collections::HashSet; +use super::*; +use super::super::telephone::*; + +#[derive(Clone)] +pub struct Telephone { + pub number: String, + pub extension: String, + pub work: bool, + pub home: bool, + pub text: bool, + pub voice: bool, + pub fax: bool, + pub cell: bool, + pub video: bool, + pub pager: bool, + pub text_phone: bool, +} + +impl VCardPropertyInputObject<properties::Telephone, TelephoneView> for Telephone { + fn new() -> Self { + Self { + number: String::new(), + extension: String::new(), + work: false, + home: false, + text: false, + voice: false, + fax: false, + cell: false, + video: false, + pager: false, + text_phone: false, + } + } + fn get_input_fields(&self, link: &ComponentLink<TelephoneView>) -> Vec<VCardPropertyInputField> { + vec![ + VCardPropertyInputField::Text{ + label: "Number".to_string(), + id: Some("number".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateNumber(e.value)), + value: self.number.clone(), + }, + VCardPropertyInputField::Text{ + label: "Extension".to_string(), + id: Some("extension".to_string()), + placeholder: None, + oninput: link.callback(|e: InputData| Msg::UpdateExtension(e.value)), + value: self.extension.clone(), + }, + VCardPropertyInputField::CheckBox{ + label: "Work".to_string(), + id: Some("work".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleWork), + value: self.work, + }, + VCardPropertyInputField::CheckBox{ + label: "Home".to_string(), + id: Some("home".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleHome), + value: self.home, + }, + VCardPropertyInputField::CheckBox{ + label: "Text".to_string(), + id: Some("text".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleText), + value: self.text, + }, + VCardPropertyInputField::CheckBox{ + label: "Voice".to_string(), + id: Some("voice".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleVoice), + value: self.voice, + }, + VCardPropertyInputField::CheckBox{ + label: "Fax".to_string(), + id: Some("fax".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleFax), + value: self.fax, + }, + VCardPropertyInputField::CheckBox{ + label: "Cell".to_string(), + id: Some("cell".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleCell), + value: self.cell, + }, + VCardPropertyInputField::CheckBox{ + label: "Video".to_string(), + id: Some("video".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleVideo), + value: self.video, + }, + VCardPropertyInputField::CheckBox{ + label: "Pager".to_string(), + id: Some("pager".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::TogglePager), + value: self.pager, + }, + VCardPropertyInputField::CheckBox{ + label: "Text Phone".to_string(), + id: Some("text_phone".to_string()), + onclick: link.callback(|_: MouseEvent| Msg::ToggleTextPhone), + value: self.text_phone, + }, + ] + } + fn to_vcard_property(&self) -> Result<properties::Telephone, VCardPropertyInputError> { + let mut telephone = properties::Telephone::from_telephone_value( + values::telephone_value::TelephoneValue::from_telephone_number_str( + self.number.clone(), + match self.extension.is_empty() { + true => None::<&str>, + false => Some(&self.extension), + }, + ).unwrap() + ); + + let type_values = { + let mut type_values = HashSet::new(); + + if self.work { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Work); + } + if self.home { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Home); + } + if self.text { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Text); + } + if self.voice { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Voice); + } + if self.fax { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Fax); + } + if self.cell { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Cell); + } + if self.video { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Video); + } + if self.pager { + type_values.insert(values::type_value::TypeValueWithTelephoneType::Pager); + } + if self.text_phone { + type_values.insert(values::type_value::TypeValueWithTelephoneType::TextPhone); + } + + vcard::Set::from_hash_set(type_values).unwrap() + }; + + if let properties::Telephone::TelephoneValue { ref mut typ, .. } = telephone { + *typ = Some(parameters::typ::TypeWithTelType::from_type_values(type_values)); + } + + Ok(telephone) + } +}
\ No newline at end of file diff --git a/src/view/input_objects/utility.rs b/src/view/input_objects/utility.rs new file mode 100644 index 0000000..a296c1e --- /dev/null +++ b/src/view/input_objects/utility.rs @@ -0,0 +1,39 @@ +#[derive(Clone)] +pub struct Download { + pub file_name: String, + pub content: String, + pub mime_type: MimeType, +} + +impl Download { + pub fn as_data_link(&self) -> String { + let data = base64::encode(&*self.content); + let uri_component: String = js_sys::encode_uri_component(&data).into(); + + format!("data:{};base64,{}", self.mime_type.as_text(), uri_component) + } +} + +#[derive(Clone, Copy)] +pub enum MimeType { + PDF, + VCard, + SVG, +} + +impl MimeType { + pub fn as_text(&self) -> &str { + match self { + MimeType::PDF => "application/pdf", + MimeType::VCard => "text/vcard", + MimeType::SVG => "image/svg+xml", + } + } +} + +#[derive(Clone, Copy)] +pub enum DownloadOption { + PDF, + VCard, + QrCode, +}
\ No newline at end of file diff --git a/src/view/main.rs b/src/view/main.rs new file mode 100644 index 0000000..e1753b4 --- /dev/null +++ b/src/view/main.rs @@ -0,0 +1,335 @@ +use crate::view::telephone::TelephoneView; +use std::collections::HashSet; +use super::name::{NameView}; +use super::address::AddressView; +use genpdf::Element as _; +use genpdf::{elements, style, fonts}; +use qrcodegen::QrCode; +use qrcodegen::QrCodeEcc; +use yew::prelude::*; +use vcard::{VCard, VCardError}; +use super::input_objects::utility::*; +use super::input_objects::name::Name; +use super::input_objects::address::Address; +use super::input_objects::telephone::Telephone; +use super::input_objects::VCardPropertyInputObject; + + +pub struct MainView { + link: ComponentLink<Self>, + error: Vec<String>, + name: Name, + address: Address, + telephone: Telephone, + download: Option<Download>, + selected_option: DownloadOption, +} + +pub enum Msg { + UpdateName(Name), + UpdateAddress(Address), + UpdateTelephone(Telephone), + Generate(DownloadOption), + Nope, +} + +impl Component for MainView { + type Message = Msg; + type Properties = (); + + fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { + MainView { + link, + error: vec![], + name: Name::new(), + address: Address::new(), + telephone: Telephone::new(), + 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; + self.link.send_message(Msg::Generate(self.selected_option)); + }, + Msg::UpdateAddress(value) => { + self.address = value; + self.link.send_message(Msg::Generate(self.selected_option)); + }, + Msg::UpdateTelephone(value) => { + self.telephone = 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 + } + }; + + 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, + } + ) + } + } + 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, + }; + if self.error.len() > 0 { + self.download = None; + } + true + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + + let download_options = self.link.callback(|e: ChangeData| + match e { + ChangeData::Select(v) => match v.value().as_str() { + "vcard" => Msg::Generate(DownloadOption::VCard), + "pdf" => Msg::Generate(DownloadOption::PDF), + "qrcode" => Msg::Generate(DownloadOption::QrCode), + _ => Msg::Nope, + }, + _ => Msg::Nope, + } + ); + + html!{ + <> + <main> + <section class="hero"> + <div class="hero-body"> + <div class="container is-max-widescreen"> + <h1 class="title">{ "A Generator for vCards" }</h1> + <h2 class="subtitle">{ "Supports generating vCards (.vcf), print-ready PDF business cards and QR Codes" }</h2> + </div> + </div> + </section> + + <section class="section"> + <div class="container is-max-widescreen"> + + { self.render_errors() } + + <NameView oninput=self.link.callback(|n: Name| Msg::UpdateName(n)) /> + + <AddressView oninput=self.link.callback(|a: Address| Msg::UpdateAddress(a)) /> + + <TelephoneView oninput=self.link.callback(|t: Telephone| Msg::UpdateTelephone(t)) /> + + <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> + <option value="qrcode">{ "QR Code" }</option> + </select> + </div> + + { self.render_download() } + </div> + + <div class="block"> + { self.render_preview() } + </div> + + </div> + </section> + </main> + + <footer class="footer"> + <div class="content has-text-centered"> + <p> + <strong>{ "VCard Generator" }</strong> { " by " } <a href="https://jelemux.dev">{ "Jeremias Weber" }</a>{ ". "} + { "The source code is licenced " } <a href="http://opensource.org/licenses/mit-license.php">{ "MIT" }</a>{"."} + </p> + </div> + </footer> + </> + } + } +} + +impl MainView { + fn render_errors(&self) -> Html { + html!{ + <> + { + for self.error.iter().map(|err| + html!{ + <div class="notification is-danger is-light"> + { err } + </div> + } + ) + } + </> + } + } + fn render_download(&self) -> Html { + if self.download.is_some() { + let download = self.download.as_ref().unwrap(); + + html!{ + <a href=download.as_data_link() download=download.file_name class="button is-primary level-item" > + { "Download" } + </a> + } + } else { + html!{} + } + } + fn render_preview(&self) -> Html { + if self.download.is_some() { + let download = self.download.as_ref().unwrap(); + + match download.mime_type { + MimeType::PDF => html!{ + <iframe src=download.as_data_link() alt="PDF Preview" width="400" height="550"/> + }, + 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"/> + }, + } + } else { + html!{} + } + } + fn generate_vcard(&self) -> Result<VCard, VCardError> { + match VCard::from_formatted_name_str(&self.name.formatted_name()) { + Ok(vcard) => { + let mut vcard = vcard; + + let names = { + let mut names = HashSet::new(); + names.insert(self.name.to_vcard_property().unwrap()); + + names + }; + + let addresses = { + let mut addresses = HashSet::new(); + addresses.insert(self.address.to_vcard_property().unwrap()); + + addresses + }; + + let telephones = { + let mut telephones = HashSet::new(); + telephones.insert(self.telephone.to_vcard_property().unwrap()); + + telephones + }; + + vcard.names = Some(vcard::Set::from_hash_set(names).unwrap()); + vcard.addresses = Some(vcard::Set::from_hash_set(addresses).unwrap()); + vcard.telephones = Some(vcard::Set::from_hash_set(telephones).unwrap()); + + Ok(vcard) + } + Err(err) => Err(err), + } + } + fn generate_pdf(&self) -> Result<String, ()>{ + let regular_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Regular.ttf"); + let regular_font_data = fonts::FontData::new(regular_bytes.to_vec(), Some(printpdf::BuiltinFont::Helvetica)).expect("font data should be correct"); + + let bold_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Bold.ttf"); + let bold_font_data = fonts::FontData::new(bold_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBold)).expect("font data should be correct"); + + let italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Italic.ttf"); + let italic_font_data = fonts::FontData::new(italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaOblique)).expect("font data should be correct"); + + let bold_italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-BoldItalic.ttf"); + let bold_italic_font_data = fonts::FontData::new(bold_italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBoldOblique)).expect("font data should be correct"); + + let font_family = fonts::FontFamily{ + regular: regular_font_data, + bold: bold_font_data, + italic: italic_font_data, + bold_italic: bold_italic_font_data + }; + + let mut doc = genpdf::Document::new(font_family); + + doc.set_title("BCard test"); + doc.set_minimal_conformance(); + doc.set_margins(10); + doc.set_line_spacing(1.25); + + doc.push( + elements::Paragraph::new("genpdf Demo Document") + .aligned(elements::Alignment::Center) + .styled(style::Style::new().bold().with_font_size(20)), + ); + + // TODO fill doc with real data + + let mut buf: Vec<u8> = Vec::new(); + match doc.render(&mut buf) { + Ok(_) => Ok(match String::from_utf8(buf) { + Ok(s) => s, + Err(_) => return Err(()), + }), + Err(_) => Err(()), + } + } +}
\ No newline at end of file diff --git a/src/view/mod.rs b/src/view/mod.rs index d34eb39..b46accf 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -1,262 +1,26 @@ -use crate::view::telephone::{TelephoneView,Telephone}; -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; -use qrcodegen::QrCodeEcc; use yew::prelude::*; -use vcard::{VCard, VCardError}; - - -mod name; -mod photo; -mod birthday; -mod address; -mod telephone; - -#[derive(Clone)] -pub struct Download { - pub file_name: String, - pub content: String, - pub mime_type: MimeType, -} - -impl Download { - pub fn as_data_link(&self) -> String { - let data = base64::encode(&*self.content); - let uri_component: String = js_sys::encode_uri_component(&data).into(); - - format!("data:{};base64,{}", self.mime_type.as_text(), uri_component) - } -} - -#[derive(Clone, Copy)] -pub enum MimeType { - PDF, - VCard, - SVG, -} - -impl MimeType { - pub fn as_text(&self) -> &str { - match self { - MimeType::PDF => "application/pdf", - MimeType::VCard => "text/vcard", - MimeType::SVG => "image/svg+xml", - } - } -} - -#[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, - telephone: Telephone, - download: Option<Download>, - selected_option: DownloadOption, -} - -pub enum Msg { - UpdateName(Name), - UpdateHomeAddress(Address), - UpdateWorkAddress(Address), - UpdateTelephone(Telephone), - Generate(DownloadOption), - Nope, -} - -impl Component for MainView { - type Message = Msg; - type Properties = (); - - fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { - MainView { - link, - error: vec![], - name: Name::new(), - work_address: Address::new_with_type(AddressType::Work), - home_address: Address::new_with_type(AddressType::Home), - telephone: Telephone::new(), - 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; - 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::UpdateTelephone(value) => { - self.telephone = 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 - } - }; - - 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, - } - ) - } - } - 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, - }; - if self.error.len() > 0 { - self.download = None; - } - true - } - - fn change(&mut self, _props: Self::Properties) -> ShouldRender { - false - } - - fn view(&self) -> Html { - - let download_options = self.link.callback(|e: ChangeData| - match e { - ChangeData::Select(v) => match v.value().as_str() { - "vcard" => Msg::Generate(DownloadOption::VCard), - "pdf" => Msg::Generate(DownloadOption::PDF), - "qrcode" => Msg::Generate(DownloadOption::QrCode), - _ => Msg::Nope, - }, - _ => Msg::Nope, - } - ); - - html!{ - <> - <main> - <section class="hero"> - <div class="hero-body"> - <div class="container is-max-widescreen"> - <h1 class="title">{ "A Generator for vCards" }</h1> - <h2 class="subtitle">{ "Supports generating vCards (.vcf), print-ready PDF business cards and QR Codes" }</h2> - </div> - </div> - </section> - - <section class="section"> - <div class="container is-max-widescreen"> - - { self.render_errors() } - - <NameView oninput=self.link.callback(|n: Name| Msg::UpdateName(n)) /> - - <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)) /> - - <TelephoneView oninput=self.link.callback(|t: Telephone| Msg::UpdateTelephone(t)) /> - - <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> - <option value="qrcode">{ "QR Code" }</option> - </select> - </div> - - { self.render_download() } - </div> - - <div class="block"> - { self.render_preview() } - </div> - - </div> - </section> - </main> - - <footer class="footer"> - <div class="content has-text-centered"> - <p> - <strong>{ "VCard Generator" }</strong> { " by " } <a href="https://jelemux.dev">{ "Jeremias Weber" }</a>{ ". "} - { "The source code is licenced " } <a href="http://opensource.org/licenses/mit-license.php">{ "MIT" }</a>{"."} - </p> - </div> - </footer> - </> - } - } -} - -impl MainView { +use input_objects::*; +pub mod input_objects; + +pub mod main; +pub mod name; +pub mod photo; +pub mod birthday; +pub mod address; +pub mod telephone; + +pub trait VCardPropertyInputComponent<P, T>: Component + where P: vcard::properties::Property, + T: VCardPropertyInputObject<P, Self> +{ + fn get_input_object(&self) -> T; + fn get_title(&self) -> String; + fn get_errors(&self) -> Vec<String>; fn render_errors(&self) -> Html { html!{ <> { - for self.error.iter().map(|err| + for self.get_errors().iter().map(|err| html!{ <div class="notification is-danger is-light"> { err } @@ -267,120 +31,4 @@ impl MainView { </> } } - fn render_download(&self) -> Html { - if self.download.is_some() { - let download = self.download.as_ref().unwrap(); - - html!{ - <a href=download.as_data_link() download=download.file_name class="button is-primary level-item" > - { "Download" } - </a> - } - } else { - html!{} - } - } - fn render_preview(&self) -> Html { - if self.download.is_some() { - let download = self.download.as_ref().unwrap(); - - match download.mime_type { - MimeType::PDF => html!{ - <iframe src=download.as_data_link() alt="PDF Preview" width="400" height="550"/> - }, - 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"/> - }, - } - } else { - html!{} - } - } - fn generate_vcard(&self) -> Result<VCard, VCardError> { - match VCard::from_formatted_name_str(&self.name.formatted_name()) { - Ok(vcard) => { - let mut vcard = vcard; - - let names = { - let mut names = HashSet::new(); - names.insert(self.name.to_vcard_value()); - - names - }; - - let addresses = { - let mut addresses = HashSet::new(); - addresses.insert(self.home_address.to_vcard_value()); - addresses.insert(self.work_address.to_vcard_value()); - - addresses - }; - - let telephones = { - let mut telephones = HashSet::new(); - telephones.insert(self.telephone.to_vcard_value()); - - telephones - }; - - vcard.names = Some(vcard::Set::from_hash_set(names).unwrap()); - vcard.addresses = Some(vcard::Set::from_hash_set(addresses).unwrap()); - vcard.telephones = Some(vcard::Set::from_hash_set(telephones).unwrap()); - - Ok(vcard) - } - Err(err) => Err(err), - } - } - fn generate_pdf(&self) -> Result<String, ()>{ - let regular_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Regular.ttf"); - let regular_font_data = fonts::FontData::new(regular_bytes.to_vec(), Some(printpdf::BuiltinFont::Helvetica)).expect("font data should be correct"); - - let bold_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Bold.ttf"); - let bold_font_data = fonts::FontData::new(bold_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBold)).expect("font data should be correct"); - - let italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Italic.ttf"); - let italic_font_data = fonts::FontData::new(italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaOblique)).expect("font data should be correct"); - - let bold_italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-BoldItalic.ttf"); - let bold_italic_font_data = fonts::FontData::new(bold_italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBoldOblique)).expect("font data should be correct"); - - let font_family = fonts::FontFamily{ - regular: regular_font_data, - bold: bold_font_data, - italic: italic_font_data, - bold_italic: bold_italic_font_data - }; - - let mut doc = genpdf::Document::new(font_family); - - doc.set_title("BCard test"); - doc.set_minimal_conformance(); - doc.set_margins(10); - doc.set_line_spacing(1.25); - - doc.push( - elements::Paragraph::new("genpdf Demo Document") - .aligned(elements::Alignment::Center) - .styled(style::Style::new().bold().with_font_size(20)), - ); - - // TODO fill doc with real data - - let mut buf: Vec<u8> = Vec::new(); - match doc.render(&mut buf) { - Ok(_) => Ok(match String::from_utf8(buf) { - Ok(s) => s, - Err(_) => return Err(()), - }), - Err(_) => Err(()), - } - } }
\ No newline at end of file diff --git a/src/view/name.rs b/src/view/name.rs index 872676c..4ac78a4 100644 --- a/src/view/name.rs +++ b/src/view/name.rs @@ -1,86 +1,14 @@ use yew::prelude::*; use vcard::properties; -use vcard::values::{self, text}; - -use crate::util; - -#[derive(Clone)] -pub struct Name { - pub prefix: String, - pub first_name: String, - pub middle_name: String, - pub last_name: String, - pub suffix: String, -} - -impl Name { - pub fn new() -> Self { - Self { - prefix: String::new(), - first_name: String::new(), - middle_name: String::new(), - last_name: String::new(), - suffix: String::new(), - } - } - pub fn formatted_name(&self) -> String { - let mut formatted_name = String::new(); - - if !self.prefix.is_empty() { - formatted_name.push_str(&self.prefix); - } - if !self.first_name.is_empty() { - formatted_name.push_str(" "); - formatted_name.push_str(&self.first_name); - } - if !self.middle_name.is_empty() { - formatted_name.push_str(" "); - formatted_name.push_str(&self.middle_name); - } - if !self.last_name.is_empty() { - formatted_name.push_str(" "); - formatted_name.push_str(&self.last_name); - } - if !self.suffix.is_empty() { - formatted_name.push_str(", "); - formatted_name.push_str(&self.suffix); - } - - formatted_name - } - pub fn to_vcard_value(&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) - } -} +use super::input_objects::name::*; +use super::input_objects::VCardPropertyInputObject; +use super::VCardPropertyInputComponent; pub struct NameView { link: ComponentLink<Self>, value: Name, oninput: Callback<Name>, - //errors: Vec<String>, + errors: Vec<String>, } pub enum Msg { @@ -94,7 +22,18 @@ pub enum Msg { #[derive(Clone, PartialEq, Properties)] pub struct Props { pub oninput: Callback<Name>, - //pub errors: Vec<String>, +} + +impl VCardPropertyInputComponent<properties::Name, Name> for NameView { + fn get_input_object(&self) -> Name { + self.value.clone() + } + fn get_title(&self) -> String { + "Name".to_string() + } + fn get_errors(&self) -> Vec<String> { + self.errors.clone() + } } impl Component for NameView { @@ -105,6 +44,7 @@ impl Component for NameView { link, value: Name::new(), oninput: props.oninput, + errors: vec![], } } fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { @@ -125,46 +65,16 @@ impl Component for NameView { fn view(&self) -> yew::virtual_dom::VNode { html!{ <div class="box"> - <h3 class="subtitle">{ "Name" }</h3> - - <div class="columns is-mobile is-multiline"> - - { util::text_field_input( - "Prefix", - "prefix", - Some("Sir"), - self.link.callback(|e: InputData| Msg::UpdatePrefix(e.value)) - ) } - - { util::text_field_input( - "First name", - "first_name", - Some("Arthur"), - self.link.callback(|e: InputData| Msg::UpdateFirstName(e.value)) - ) } - - { util::text_field_input( - "Middle name", - "middle_name", - Some("Charles"), - self.link.callback(|e: InputData| Msg::UpdateMiddleName(e.value)) - ) } + { + self.render_errors() + } - { util::text_field_input( - "Last name", - "last_name", - Some("Clarke"), - self.link.callback(|e: InputData| Msg::UpdateLastName(e.value)) - ) } + <h3 class="subtitle">{ self.get_title() }</h3> - { util::text_field_input( - "Suffix", - "suffix", - Some("CBE FRAS"), - self.link.callback(|e: InputData| Msg::UpdateSuffix(e.value)) - ) } + { + self.get_input_object().render(&self.link) + } - </div> </div> } } diff --git a/src/view/telephone.rs b/src/view/telephone.rs index 28a6f01..516552e 100644 --- a/src/view/telephone.rs +++ b/src/view/telephone.rs @@ -1,100 +1,14 @@ -use yew::services::ConsoleService; use yew::prelude::*; use vcard::properties; -use vcard::parameters; -use vcard::values::{self, text}; -use vcard::validators::ValidatedWrapper; -use std::collections::HashSet; -use crate::util; - -#[derive(Clone)] -pub struct Telephone { - pub number: String, - pub extension: String, - pub work: bool, - pub home: bool, - pub text: bool, - pub voice: bool, - pub fax: bool, - pub cell: bool, - pub video: bool, - pub pager: bool, - pub text_phone: bool, -} - -impl Telephone { - pub fn new() -> Self { - Self { - number: String::new(), - extension: String::new(), - work: false, - home: false, - text: false, - voice: false, - fax: false, - cell: false, - video: false, - pager: false, - text_phone: false, - } - } - pub fn to_vcard_value(&self) -> properties::Telephone { - let mut telephone = properties::Telephone::from_telephone_value( - values::telephone_value::TelephoneValue::from_telephone_number_str( - self.number.clone(), - match self.extension.is_empty() { - true => None::<&str>, - false => Some(&self.extension), - }, - ).unwrap() - ); - - let type_values = { - let mut type_values = HashSet::new(); - - if self.work { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Work); - } - if self.home { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Home); - } - if self.text { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Text); - } - if self.voice { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Voice); - } - if self.fax { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Fax); - } - if self.cell { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Cell); - } - if self.video { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Video); - } - if self.pager { - type_values.insert(values::type_value::TypeValueWithTelephoneType::Pager); - } - if self.text_phone { - type_values.insert(values::type_value::TypeValueWithTelephoneType::TextPhone); - } - - vcard::Set::from_hash_set(type_values).unwrap() - }; - - if let properties::Telephone::TelephoneValue { ref mut typ, .. } = telephone { - *typ = Some(parameters::typ::TypeWithTelType::from_type_values(type_values)); - } - - telephone - } -} +use super::input_objects::telephone::*; +use super::input_objects::VCardPropertyInputObject; +use super::VCardPropertyInputComponent; pub struct TelephoneView { link: ComponentLink<Self>, value: Telephone, oninput: Callback<Telephone>, + errors: Vec<String>, } pub enum Msg { @@ -117,6 +31,18 @@ pub struct Props { //pub errors: Vec<String>, } +impl VCardPropertyInputComponent<properties::Telephone, Telephone> for TelephoneView { + fn get_input_object(&self) -> Telephone { + self.value.clone() + } + fn get_title(&self) -> String { + "Telephone".to_string() + } + fn get_errors(&self) -> Vec<String> { + self.errors.clone() + } +} + impl Component for TelephoneView { type Message = Msg; type Properties = Props; @@ -125,6 +51,7 @@ impl Component for TelephoneView { link, value: Telephone::new(), oninput: props.oninput, + errors: vec![], } } fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { @@ -151,32 +78,16 @@ impl Component for TelephoneView { fn view(&self) -> yew::virtual_dom::VNode { html!{ <div class="box"> - <h3 class="subtitle">{ "Telephone" }</h3> - - <div class="columns is-mobile is-multiline"> - - { util::text_field_input( - "Number", - "number", - None, - self.link.callback(|e: InputData| Msg::UpdateNumber(e.value)) - ) } + { + self.render_errors() + } - { util::text_field_input( - "Extension", - "extension", - None, - self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) - ) } + <h3 class="subtitle">{ self.get_title() }</h3> - { util::checkbox_field_input( - "Work", - "work", - self.value.work, - self.link.callback(|_| Msg::ToggleWork) - ) } + { + self.get_input_object().render(&self.link) + } - </div> </div> } } |