diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/address.rs | 213 | ||||
-rw-r--r-- | src/view/birthday.rs | 11 | ||||
-rw-r--r-- | src/view/mod.rs | 386 | ||||
-rw-r--r-- | src/view/name.rs | 171 | ||||
-rw-r--r-- | src/view/photo.rs | 10 | ||||
-rw-r--r-- | src/view/telephone.rs | 183 |
6 files changed, 974 insertions, 0 deletions
diff --git a/src/view/address.rs b/src/view/address.rs new file mode 100644 index 0000000..a30ba85 --- /dev/null +++ b/src/view/address.rs @@ -0,0 +1,213 @@ +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", + } + } +} + +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"> + + { 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)) + ) } + + { util::text_field_input( + "Postal Code", + "code", + None, + self.link.callback(|e: InputData| Msg::UpdateCode(e.value)) + ) } + + { util::text_field_input( + "Country", + "country", + None, + self.link.callback(|e: InputData| Msg::UpdateCountry(e.value)) + ) } + + </div> + </div> + } + } +}
\ No newline at end of file diff --git a/src/view/birthday.rs b/src/view/birthday.rs new file mode 100644 index 0000000..d4b9356 --- /dev/null +++ b/src/view/birthday.rs @@ -0,0 +1,11 @@ +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; + +#[derive(Clone)] +pub struct Birthday { + pub year: u16, + pub month: u8, + pub day: u8, +}
\ No newline at end of file diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..d34eb39 --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,386 @@ +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 { + 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_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 new file mode 100644 index 0000000..872676c --- /dev/null +++ b/src/view/name.rs @@ -0,0 +1,171 @@ +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) + } +} + +pub struct NameView { + link: ComponentLink<Self>, + value: Name, + oninput: Callback<Name>, + //errors: Vec<String>, +} + +pub enum Msg { + UpdatePrefix(String), + UpdateFirstName(String), + UpdateMiddleName(String), + UpdateLastName(String), + UpdateSuffix(String), +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + pub oninput: Callback<Name>, + //pub errors: Vec<String>, +} + +impl Component for NameView { + type Message = Msg; + type Properties = Props; + fn create(props: <Self as yew::Component>::Properties, link: yew::html::Scope<Self>) -> Self { + Self { + link, + value: Name::new(), + oninput: props.oninput, + } + } + fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { + match msg { + Msg::UpdatePrefix(p) => self.value.prefix = p, + Msg::UpdateFirstName(f) => self.value.first_name = f, + Msg::UpdateMiddleName(m) => self.value.middle_name = m, + Msg::UpdateLastName(l) => self.value.last_name = l, + Msg::UpdateSuffix(s) => self.value.suffix = s, + }; + self.oninput.emit(self.value.clone()); + true + } + fn change(&mut self, props: <Self as yew::Component>::Properties) -> bool { + self.oninput = props.oninput; + true + } + 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)) + ) } + + { util::text_field_input( + "Last name", + "last_name", + Some("Clarke"), + self.link.callback(|e: InputData| Msg::UpdateLastName(e.value)) + ) } + + { util::text_field_input( + "Suffix", + "suffix", + Some("CBE FRAS"), + self.link.callback(|e: InputData| Msg::UpdateSuffix(e.value)) + ) } + + </div> + </div> + } + } +}
\ No newline at end of file diff --git a/src/view/photo.rs b/src/view/photo.rs new file mode 100644 index 0000000..1c2d088 --- /dev/null +++ b/src/view/photo.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; + +#[derive(Clone)] +pub struct Photo { + pub base64_image: String, +} + diff --git a/src/view/telephone.rs b/src/view/telephone.rs new file mode 100644 index 0000000..28a6f01 --- /dev/null +++ b/src/view/telephone.rs @@ -0,0 +1,183 @@ +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 + } +} + +pub struct TelephoneView { + link: ComponentLink<Self>, + value: Telephone, + oninput: Callback<Telephone>, +} + +pub enum Msg { + UpdateNumber(String), + UpdateExtension(String), + ToggleWork, + ToggleHome, + ToggleText, + ToggleVoice, + ToggleFax, + ToggleCell, + ToggleVideo, + TogglePager, + ToggleTextPhone, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + pub oninput: Callback<Telephone>, + //pub errors: Vec<String>, +} + +impl Component for TelephoneView { + type Message = Msg; + type Properties = Props; + fn create(props: <Self as yew::Component>::Properties, link: yew::html::Scope<Self>) -> Self { + Self { + link, + value: Telephone::new(), + oninput: props.oninput, + } + } + fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { + match msg { + Msg::UpdateNumber(n) => self.value.number = n, + Msg::UpdateExtension(e) => self.value.extension = e, + Msg::ToggleWork => self.value.work = !self.value.work, + Msg::ToggleHome => self.value.home = !self.value.home, + Msg::ToggleText => self.value.text = !self.value.text, + Msg::ToggleVoice => self.value.voice = !self.value.voice, + Msg::ToggleFax => self.value.fax = !self.value.fax, + Msg::ToggleCell => self.value.cell = !self.value.cell, + Msg::ToggleVideo => self.value.video = !self.value.video, + Msg::TogglePager => self.value.pager = !self.value.pager, + Msg::ToggleTextPhone => self.value.text_phone = !self.value.text_phone, + }; + self.oninput.emit(self.value.clone()); + true + } + fn change(&mut self, props: <Self as yew::Component>::Properties) -> bool { + self.oninput = props.oninput; + true + } + 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)) + ) } + + { util::text_field_input( + "Extension", + "extension", + None, + self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) + ) } + + { util::checkbox_field_input( + "Work", + "work", + self.value.work, + self.link.callback(|_| Msg::ToggleWork) + ) } + + </div> + </div> + } + } +}
\ No newline at end of file |