summaryrefslogtreecommitdiff
path: root/src/view
diff options
context:
space:
mode:
authorjelemux <jeremias.weber@protonmail.com>2020-11-19 07:37:20 +0100
committerjelemux <jeremias.weber@protonmail.com>2020-11-19 07:37:20 +0100
commit49588f22f7d20193f899226107c9e323a82c6951 (patch)
tree7f7bb739336f87aa2c950038f7d5a7e154f09dbd /src/view
parent104f70b0968d7138d6cf944da98d95a405b1a049 (diff)
downloadwasm-card-49588f22f7d20193f899226107c9e323a82c6951.tar.gz
wasm-card-49588f22f7d20193f899226107c9e323a82c6951.tar.bz2
added telephone, but causes problems
Diffstat (limited to 'src/view')
-rw-r--r--src/view/address.rs213
-rw-r--r--src/view/birthday.rs11
-rw-r--r--src/view/mod.rs386
-rw-r--r--src/view/name.rs171
-rw-r--r--src/view/photo.rs10
-rw-r--r--src/view/telephone.rs183
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