summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjelemux <jeremias.weber@protonmail.com>2020-11-24 21:58:51 +0100
committerjelemux <jeremias.weber@protonmail.com>2020-11-24 21:58:51 +0100
commit5a03734b6767fed04c0913384584d8f59dc597ea (patch)
tree62ef0a0afc0ab56dd36da1a662db768fd8c22eda
parent49588f22f7d20193f899226107c9e323a82c6951 (diff)
downloadwasm-card-5a03734b6767fed04c0913384584d8f59dc597ea.tar.gz
wasm-card-5a03734b6767fed04c0913384584d8f59dc597ea.tar.bz2
add traits for viewmodel and view
-rw-r--r--src/lib.rs3
-rw-r--r--src/util.rs39
-rw-r--r--src/view/address.rs181
-rw-r--r--src/view/input_objects/address.rs151
-rw-r--r--src/view/input_objects/mod.rs106
-rw-r--r--src/view/input_objects/name.rs118
-rw-r--r--src/view/input_objects/telephone.rs162
-rw-r--r--src/view/input_objects/utility.rs39
-rw-r--r--src/view/main.rs335
-rw-r--r--src/view/mod.rs388
-rw-r--r--src/view/name.rs138
-rw-r--r--src/view/telephone.rs137
12 files changed, 1007 insertions, 790 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 6982920..2184f44 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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>
}
}