├── js └── index.js ├── .gitignore ├── docs ├── kp-chart.wasm ├── index.html ├── styles.css └── kp-chart.js ├── src ├── web │ ├── mod.rs │ ├── root.rs │ ├── chart.rs │ └── people.rs ├── data │ ├── mod.rs │ ├── week.rs │ ├── job.rs │ ├── person.rs │ └── day.rs └── lib.rs ├── static ├── index.html └── styles.css ├── README.md ├── package.json ├── Cargo.toml ├── webpack.config.js ├── Makefile └── Cargo.lock /js/index.js: -------------------------------------------------------------------------------- 1 | import("../pkg/index.js").catch(console.error); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /pkg 3 | /node_modules 4 | .cache/ 5 | dist/ 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /docs/kp-chart.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluejekyll/kp-chart/HEAD/docs/kp-chart.wasm -------------------------------------------------------------------------------- /src/web/mod.rs: -------------------------------------------------------------------------------- 1 | mod chart; 2 | mod people; 3 | mod root; 4 | 5 | pub use self::chart::Chart; 6 | pub use self::people::PeopleModel; 7 | pub use self::root::RootModel; 8 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | mod day; 2 | mod job; 3 | mod person; 4 | mod week; 5 | 6 | pub use self::day::Day; 7 | pub use self::job::Job; 8 | pub use self::person::{Ability, Person}; 9 | pub use self::week::Week; 10 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kitchen Patrol 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kitchen Patrol 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/data/week.rs: -------------------------------------------------------------------------------- 1 | use crate::data::{Day, Job}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Week { 5 | week: Vec, 6 | } 7 | 8 | impl Week { 9 | pub fn new(week: Vec) -> Self { 10 | Self { week } 11 | } 12 | 13 | pub fn num_jobs(&self) -> usize { 14 | self.week[0].jobs().len() 15 | } 16 | 17 | pub fn days(&self) -> &[Day] { 18 | &self.week 19 | } 20 | 21 | pub fn jobs(&self) -> impl Iterator { 22 | self.week[0].jobs().iter().map(|(job, _)| job) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kitchen Patrol Charting Application 2 | 3 | An example application, written in Rust, by someone with little frontend experience 4 | 5 | ## Usage 6 | 7 | Browse to [https://bluejekyll.github.io/kp-chart/](https://bluejekyll.github.io/kp-chart/) 8 | 9 | ## Building 10 | 11 | - Initililize once 12 | 13 | ```console 14 | $> make init 15 | ``` 16 | 17 | - Build 18 | 19 | ```console 20 | $> make build 21 | ``` 22 | 23 | - Start a local appserver 24 | 25 | ```console 26 | $> make run 27 | ``` 28 | 29 | ## Deploying 30 | 31 | ```console 32 | $> make deploy 33 | ``` 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kp-chart", 3 | "version": "1.0.0", 4 | "description": "To keep up the contributions to the dollar jar.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "webpack-dev-server": "^3.10.3" 8 | }, 9 | "devDependencies": { 10 | "copy-webpack-plugin": "^5.1.1", 11 | "@wasm-tool/wasm-pack-plugin": "^1.2.0", 12 | "webpack": "^4.42.0", 13 | "webpack-cli": "^3.3.11", 14 | "base64-loader": "^1.0.0" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --open -d", 18 | "test": "wasm-pack test --headless", 19 | "build": "webpack" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC" 24 | } 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kp-chart" 3 | version = "0.2.0" 4 | authors = ["Benjamin Fry "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "kp_chart" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | console_log = "0.1.2" 13 | futures = "0.3.4" 14 | log = "0.4.8" 15 | serde = "1.0" 16 | serde_derive = "1.0" 17 | serde_json = "1.0" 18 | web-sys = { version = "0.3.36", features = ['Document', 'Element', 'HtmlElement', 'Node', 'Window', 'RtcDataChannel', 'RtcDataChannelInit', 'RtcPeerConnection', 'RtcSessionDescription', 'RtcSessionDescriptionInit', 'RtcSdpType', 'RtcOfferOptions', 'RtcConfiguration', 'RtcIceTransportPolicy'] } 19 | wasm-bindgen = "0.2" 20 | wasm-bindgen-futures = "0.4.9" 21 | yew = { version = "0.13.0", features = ["web_sys"] } 22 | -------------------------------------------------------------------------------- /src/data/job.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use crate::data::Ability; 4 | 5 | #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] 6 | pub struct Job { 7 | name: String, 8 | people: Vec, 9 | } 10 | 11 | impl Job { 12 | pub fn new(name: &'static str, people: Vec) -> Self { 13 | Self { 14 | name: name.to_string(), 15 | people, 16 | } 17 | } 18 | 19 | pub fn name(&self) -> &str { 20 | &self.name 21 | } 22 | 23 | pub fn people(&self) -> &[Ability] { 24 | &self.people 25 | } 26 | } 27 | 28 | impl Display for Job { 29 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { 30 | write!(fmt, "{}", self.name) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin') 4 | 5 | module.exports = { 6 | entry: './js/index.js', 7 | output: { 8 | filename: 'kp-chart.js', 9 | path: path.resolve(__dirname, 'dist'), 10 | }, 11 | stats: "errors-only", 12 | plugins: [ 13 | new CopyPlugin([ 14 | { 15 | from: path.resolve(__dirname, "static"), 16 | to: path.resolve(__dirname, 'dist') 17 | } 18 | ], 19 | { logLevel: 'warn' } 20 | ), 21 | new WasmPackPlugin({ 22 | crateDirectory: __dirname, // Define where the root of the rust code is located (where the cargo.toml file is located) 23 | }), 24 | ] 25 | }; -------------------------------------------------------------------------------- /src/web/root.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use yew::prelude::*; 3 | 4 | use crate::web::*; 5 | 6 | pub struct RootModel { 7 | people_version: usize, 8 | link: ComponentLink, 9 | } 10 | 11 | pub enum RootMsg { 12 | PeopleUpdated(usize), 13 | } 14 | 15 | impl Component for RootModel { 16 | // Some details omitted. Explore the examples to get more. 17 | 18 | type Message = RootMsg; 19 | type Properties = (); 20 | 21 | fn create(_props: Self::Properties, link: ComponentLink) -> Self { 22 | RootModel { 23 | people_version: 0, 24 | link, 25 | } 26 | } 27 | 28 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 29 | match msg { 30 | RootMsg::PeopleUpdated(version) => { 31 | debug!("root people version: {}", version); 32 | if self.people_version != version { 33 | self.people_version = version; 34 | true 35 | } else { 36 | false 37 | } 38 | } 39 | } 40 | } 41 | 42 | fn view(&self) -> Html { 43 | html! { 44 |
45 |

{"Kitchen Patrol Charts"}

46 | 47 | 48 |
49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET_DIR ?= ./target 2 | CARGO_TARGET_DIR := ${TARGET_DIR} 3 | MODE ?= development 4 | RUSTFLAGS := -Ctarget-cpu=generic 5 | TMP_DIST_DIR := /tmp/kp-chart-dist 6 | 7 | .PHONY: init 8 | init: 9 | @echo "========> $@" 10 | @rustup --version || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh) 11 | rustup self update 12 | rustup update 13 | rustup target add wasm32-unknown-unknown 14 | cargo install wasm-pack 15 | npm --version 16 | npm install 17 | 18 | .PHONY: build 19 | wasm: 20 | @echo "========> $@" 21 | wasm-pack build 22 | 23 | .PHONY: build 24 | build: 25 | @echo "========> $@" 26 | npm run build -- --mode=${MODE} 27 | 28 | .PHONY: test 29 | test: build 30 | @echo "========> $@" 31 | cargo test 32 | npm run test 33 | 34 | .PHONY: run 35 | run: build 36 | @echo "========> $@" 37 | npm run start 38 | 39 | .PHONY: clean 40 | clean: 41 | @echo "========> $@" 42 | rm -rf ./pkg 43 | rm -rf ./dist 44 | 45 | 46 | .PHONY: deploy 47 | deploy: clean 48 | @echo "========> $@" 49 | @git --version 50 | 51 | # build the project 52 | $(MAKE) MODE=production WASM_MODE=--release build 53 | 54 | # deploy 55 | git worktree add ${TMP_DIST_DIR} gh-pages 56 | rm -rf ${TMP_DIST_DIR}/* 57 | cp -rp dist/* ${TMP_DIST_DIR} 58 | cd ${TMP_DIST_DIR} && \ 59 | git add -A && \ 60 | git diff --staged --quiet || \ 61 | (git commit -m "deployed on $(shell date) by ${USER}" && \ 62 | git push origin gh-pages) 63 | $(MAKE) clean_worktree 64 | 65 | .PHONY: clean_worktree 66 | clean_worktree: 67 | @echo "========> $@" 68 | rm -rf ${TMP_DIST_DIR} 69 | git worktree prune 70 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | text-align: center; 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | h1 { 10 | font-family: Georgia, serif; 11 | font-style: italic; 12 | } 13 | 14 | table { 15 | margin: 20px; 16 | background-color: cornsilk; 17 | box-shadow: 5px 5px 5px #aaaaaa; 18 | text-align: left; 19 | border-collapse: collapse; 20 | border: 1px solid #dddddd; 21 | } 22 | 23 | table th, table td { 24 | padding: 5px; 25 | } 26 | 27 | thead { 28 | padding: 10px; 29 | border: 1px; 30 | font-size: 12pt; 31 | font-style: normal; 32 | border-top-color: #c50d0d; 33 | border-top-width: 1px; 34 | border-top-style: solid; 35 | border-bottom-color: #290dc5; 36 | border-bottom-width: 5px; 37 | border-bottom-style: double; 38 | } 39 | 40 | tbody td { 41 | font-family: "Chalkboard", "ChalkboardSE-Regular", "Comic Sans", "Comic Sans MS", sans-serif; 42 | font-variant: small-caps; 43 | font-weight: bold; 44 | font-size: 16pt; 45 | color: crimson; 46 | } 47 | 48 | tbody th { 49 | border: 1px; 50 | font-size: 12pt; 51 | font-style: normal; 52 | 53 | } 54 | 55 | tbody th, tbody td { 56 | border-bottom-color: #000000; 57 | border-bottom-width: 1px; 58 | border-bottom-style: solid; 59 | border-right-color: #cacaca; 60 | border-right-width: 1px; 61 | border-right-style: solid;} 62 | 63 | tfoot td { 64 | border: none; 65 | } 66 | 67 | .edit_delete { 68 | color: black; 69 | } 70 | 71 | .disabled { 72 | color: lightgray; 73 | } 74 | 75 | i { 76 | cursor: pointer; 77 | } 78 | 79 | i:active { 80 | color: lightgray; 81 | } -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | text-align: center; 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | h1 { 10 | font-family: Georgia, serif; 11 | font-style: italic; 12 | } 13 | 14 | table { 15 | margin: 20px; 16 | background-color: cornsilk; 17 | box-shadow: 5px 5px 5px #aaaaaa; 18 | text-align: left; 19 | border-collapse: collapse; 20 | border: 1px solid #dddddd; 21 | } 22 | 23 | table th, table td { 24 | padding: 5px; 25 | } 26 | 27 | thead { 28 | padding: 10px; 29 | border: 1px; 30 | font-size: 12pt; 31 | font-style: normal; 32 | border-top-color: #c50d0d; 33 | border-top-width: 1px; 34 | border-top-style: solid; 35 | border-bottom-color: #290dc5; 36 | border-bottom-width: 5px; 37 | border-bottom-style: double; 38 | } 39 | 40 | tbody td { 41 | font-family: "Chalkboard", "ChalkboardSE-Regular", "Comic Sans", "Comic Sans MS", sans-serif; 42 | font-variant: small-caps; 43 | font-weight: bold; 44 | font-size: 16pt; 45 | color: crimson; 46 | } 47 | 48 | tbody th { 49 | border: 1px; 50 | font-size: 12pt; 51 | font-style: normal; 52 | 53 | } 54 | 55 | tbody th, tbody td { 56 | border-bottom-color: #000000; 57 | border-bottom-width: 1px; 58 | border-bottom-style: solid; 59 | border-right-color: #cacaca; 60 | border-right-width: 1px; 61 | border-right-style: solid;} 62 | 63 | tfoot td { 64 | border: none; 65 | } 66 | 67 | .edit_delete { 68 | color: black; 69 | } 70 | 71 | .disabled { 72 | color: lightgray; 73 | } 74 | 75 | i { 76 | cursor: pointer; 77 | } 78 | 79 | i:active { 80 | color: lightgray; 81 | } -------------------------------------------------------------------------------- /src/data/person.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt::{self, Display, Formatter}; 3 | 4 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 5 | pub struct Person { 6 | name: String, 7 | ability: Ability, 8 | } 9 | 10 | impl Person { 11 | pub fn new(name: &'static str, ability: Ability) -> Self { 12 | Self { 13 | name: name.to_string(), 14 | ability, 15 | } 16 | } 17 | 18 | pub fn name(&self) -> &str { 19 | &self.name 20 | } 21 | 22 | pub fn ability(&self) -> Ability { 23 | self.ability 24 | } 25 | 26 | pub fn set_name(&mut self, name: String) { 27 | self.name = name; 28 | } 29 | 30 | pub fn set_ability(&mut self, ability: Ability) { 31 | self.ability = ability; 32 | } 33 | } 34 | 35 | impl Display for Person { 36 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { 37 | write!(fmt, "{}", self.name) 38 | } 39 | } 40 | 41 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] 42 | pub enum Ability { 43 | Adult = 0, 44 | Teen = 1, 45 | Child = 2, 46 | } 47 | 48 | impl Default for Ability { 49 | fn default() -> Self { 50 | Ability::Adult 51 | } 52 | } 53 | 54 | impl Ability { 55 | pub fn enumerate() -> &'static [Ability] { 56 | &[Ability::Adult, Ability::Teen, Ability::Child] 57 | } 58 | 59 | pub fn to_str(&self) -> &'static str { 60 | match self { 61 | Ability::Adult => "Adult", 62 | Ability::Teen => "Teen", 63 | Ability::Child => "Child", 64 | } 65 | } 66 | 67 | pub fn from_i32(prim: i32) -> Self { 68 | match prim { 69 | 0 => Ability::Adult, 70 | 1 => Ability::Teen, 71 | 2 => Ability::Child, 72 | _ => panic!("bad value for Ability: {}", prim), 73 | } 74 | } 75 | } 76 | 77 | impl From for i32 { 78 | fn from(ability: Ability) -> i32 { 79 | ability as i32 80 | } 81 | } 82 | 83 | impl Display for Ability { 84 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { 85 | write!(fmt, "{}", self.to_str()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/data/day.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | use std::iter::Cycle; 3 | use std::slice::Iter; 4 | 5 | use crate::data::{Ability, Job, Person}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Day { 9 | name: String, 10 | jobs: Vec<(Job, Vec)>, 11 | } 12 | 13 | impl Day { 14 | pub fn new( 15 | name: String, 16 | jobs: Vec, 17 | children: &mut Cycle>, 18 | teens: &mut Cycle>, 19 | adults: &mut Cycle>, 20 | ) -> Self { 21 | let mut day_jobs = jobs 22 | .clone() 23 | .into_iter() 24 | .map(|j| (j, Vec::::new())) 25 | .collect::>(); 26 | 27 | // pass through all children jobs first 28 | for (job, ref mut workers) in day_jobs.iter_mut() { 29 | for ability in job.people().iter() { 30 | match *ability { 31 | Ability::Child => workers.push( 32 | children 33 | .next() 34 | .cloned() 35 | .unwrap_or_else(|| Person::new("No Child Here", Ability::Child)), 36 | ), 37 | Ability::Teen => workers.push( 38 | teens 39 | .next() 40 | .cloned() 41 | .unwrap_or_else(|| Person::new("No Teen Here", Ability::Teen)), 42 | ), 43 | Ability::Adult => workers.push( 44 | adults 45 | .next() 46 | .cloned() 47 | .unwrap_or_else(|| Person::new("No Adult Here", Ability::Adult)), 48 | ), 49 | } 50 | } 51 | } 52 | 53 | Self { 54 | name, 55 | jobs: day_jobs, 56 | } 57 | } 58 | 59 | pub fn name(&self) -> &str { 60 | &self.name 61 | } 62 | 63 | pub fn jobs(&self) -> &[(Job, Vec)] { 64 | &self.jobs 65 | } 66 | 67 | pub fn get_job_people(&self, job: usize) -> &[Person] { 68 | &self.jobs[job].1 69 | } 70 | } 71 | 72 | impl Display for Day { 73 | fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { 74 | for (job, people) in self.jobs.iter() { 75 | write!(fmt, "{}: ", job)?; 76 | for person in people.iter() { 77 | write!(fmt, "{}, ", person)?; 78 | } 79 | writeln!(fmt, "")?; 80 | } 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/web/chart.rs: -------------------------------------------------------------------------------- 1 | use log::debug; 2 | use yew::prelude::*; 3 | 4 | use crate::data::*; 5 | use crate::web::people::PeopleStore; 6 | use yew::services::{storage::Area, StorageService}; 7 | 8 | #[derive(Clone)] 9 | pub struct Chart { 10 | people_version: usize, 11 | week: Week, 12 | } 13 | 14 | #[derive(Clone, Default, PartialEq, Properties)] 15 | pub struct ChartProps { 16 | pub people_version: usize, 17 | } 18 | 19 | impl Chart { 20 | fn calculate() -> Self { 21 | debug!("calculating new week"); 22 | let mut local_store = StorageService::new(Area::Local).expect("failed to get storage"); 23 | 24 | let jobs = crate::default_jobs(); 25 | let (people_version, people) = PeopleStore::restore(&mut local_store) 26 | .map(|p| (p.inc, p.people)) 27 | .unwrap_or_else(|| (0, crate::default_people())); 28 | Self { 29 | people_version: people_version, 30 | week: crate::calculate(5, jobs, people), 31 | } 32 | } 33 | } 34 | 35 | impl Component for Chart { 36 | type Message = (); 37 | type Properties = ChartProps; 38 | 39 | fn create(_props: Self::Properties, _link: ComponentLink) -> Self { 40 | debug!("creating Chart"); 41 | Self::calculate() 42 | } 43 | 44 | fn update(&mut self, _msg: Self::Message) -> ShouldRender { 45 | false 46 | } 47 | 48 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 49 | if self.people_version != props.people_version { 50 | debug!("updating Chart"); 51 | *self = Self::calculate(); 52 | true 53 | } else { 54 | false 55 | } 56 | } 57 | 58 | fn view(&self) -> Html { 59 | let header = |name: &str| { 60 | html! { 61 | { format!("{}", name) } 62 | } 63 | }; 64 | let people_cell = |people: &[Person]| { 65 | let mut people_str = String::new(); 66 | for person in people { 67 | people_str.push_str(person.name()); 68 | people_str.push_str(", "); 69 | } 70 | 71 | html! { 72 | { people_str } 73 | } 74 | }; 75 | let job_row = |(job_idx, job): (usize, &Job)| { 76 | let days = self.week.days(); 77 | html! { 78 | { header(job.name()) } { for days.iter().map(|d| people_cell(d.get_job_people(job_idx))) } 79 | } 80 | }; 81 | 82 | html! { 83 | <> 84 |

{"Job Chart"}

85 | 86 | 87 | { for self.week.days().iter().map(|d| header(d.name())) } 88 | 89 | 90 | { for self.week.jobs().enumerate().map(|j| job_row(j)) } 91 | 92 |
{"Job"}
93 | 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "2048"] 2 | 3 | pub mod data; 4 | pub mod web; 5 | 6 | use log::Level; 7 | use std::iter::*; 8 | use wasm_bindgen::prelude::*; 9 | use yew::prelude::*; 10 | 11 | use self::data::*; 12 | 13 | pub fn default_jobs() -> Vec { 14 | let mut jobs = Vec::::new(); 15 | jobs.push(Job::new( 16 | "Breakfast dishes", 17 | vec![Ability::Teen, Ability::Child], 18 | )); 19 | jobs.push(Job::new( 20 | "Lunch preparation", 21 | vec![Ability::Adult, Ability::Adult], 22 | )); 23 | jobs.push(Job::new( 24 | "Lunch dishes", 25 | vec![Ability::Adult, Ability::Teen], 26 | )); 27 | jobs.push(Job::new( 28 | "Dinner Setting", 29 | vec![Ability::Teen, Ability::Child, Ability::Child], 30 | )); 31 | jobs.push(Job::new( 32 | "Dinner shopping and chef", 33 | vec![Ability::Adult, Ability::Adult], 34 | )); 35 | jobs.push(Job::new( 36 | "Dinner dishes", 37 | vec![Ability::Adult, Ability::Teen], 38 | )); 39 | jobs.push(Job::new("Late night dishes", vec![Ability::Teen])); 40 | jobs.push(Job::new("Cabin cleanup", vec![Ability::Adult])); 41 | jobs.push(Job::new("Nag", vec![Ability::Adult])); 42 | 43 | jobs 44 | } 45 | 46 | pub fn default_people() -> Vec { 47 | let mut people = Vec::::new(); 48 | people.push(Person::new("Grandma", Ability::Adult)); 49 | people.push(Person::new("Grandpa", Ability::Adult)); 50 | people.push(Person::new("Mom", Ability::Adult)); 51 | people.push(Person::new("Dad", Ability::Adult)); 52 | people.push(Person::new("Aunt Jane", Ability::Adult)); 53 | people.push(Person::new("Uncle Joe", Ability::Adult)); 54 | people.push(Person::new("Jackie", Ability::Teen)); 55 | people.push(Person::new("Jake", Ability::Teen)); 56 | people.push(Person::new("Jill", Ability::Child)); 57 | people.push(Person::new("Jeffrey", Ability::Child)); 58 | 59 | return people; 60 | } 61 | 62 | pub fn calculate_day_jobs() -> Week { 63 | let jobs = default_jobs(); 64 | let people = default_people(); 65 | calculate(5, jobs, people) 66 | } 67 | 68 | pub fn calculate(num_days: usize, jobs: Vec, people: Vec) -> Week { 69 | let children = people 70 | .clone() 71 | .into_iter() 72 | .filter(|p| p.ability() == Ability::Child) 73 | .collect::>(); 74 | let mut children_iter = children.iter().cycle(); 75 | 76 | let teens = people 77 | .clone() 78 | .into_iter() 79 | .filter(|p| p.ability() == Ability::Teen) 80 | .collect::>(); 81 | let mut teens_iter = teens.iter().cycle(); 82 | 83 | let adults = people 84 | .clone() 85 | .into_iter() 86 | .filter(|p| p.ability() == Ability::Adult) 87 | .collect::>(); 88 | let mut adults_iter = adults.iter().cycle(); 89 | 90 | // make sure we have a good balance of jobs across adults, we nee the count of adult jobs 91 | let adult_job_count = jobs.iter().fold(0_usize, |count, j| { 92 | j.people().iter().filter(|a| **a == Ability::Adult).count() + count 93 | }); 94 | 95 | let mut days = Vec::with_capacity(num_days); 96 | for i in 0..num_days { 97 | let day = Day::new( 98 | format!("day_{}", i), 99 | jobs.clone(), 100 | &mut children_iter, 101 | &mut teens_iter, 102 | &mut adults_iter, 103 | ); 104 | 105 | // force an additional rotation to offset Dinner duty 106 | // we need to make sure we balance the rotation of major adult jobs 107 | if (adult_job_count + 1) == adults.len() { 108 | adults_iter.next(); 109 | adults_iter.next(); 110 | } else { 111 | adults_iter.next(); 112 | } 113 | 114 | days.push(day); 115 | } 116 | 117 | Week::new(days) 118 | } 119 | 120 | #[wasm_bindgen(start)] 121 | pub fn start() -> Result<(), JsValue> { 122 | console_log::init_with_level(Level::Debug).expect("failed to initialize logger"); 123 | yew::initialize(); 124 | 125 | App::::new().mount_to_body_with_props(()); 126 | yew::run_loop(); 127 | 128 | Ok(()) 129 | } 130 | -------------------------------------------------------------------------------- /src/web/people.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error}; 2 | use serde::{Deserialize, Serialize}; 3 | use web_sys::HtmlSelectElement; 4 | use yew::callback::Callback; 5 | use yew::format::Json; 6 | use yew::prelude::*; 7 | use yew::services::{storage::Area, StorageService}; 8 | 9 | use crate::data::*; 10 | 11 | const PEOPLE_KEY: &str = "people_v1"; 12 | type IsEditting = bool; 13 | type Id = usize; 14 | 15 | pub enum PeopleMsg { 16 | AddPerson, 17 | SavePeople, 18 | EditPerson(Id), 19 | DeletePerson(Id), 20 | PersonNameInput(Id, String), 21 | PersonAbilityInput(Id, Ability), 22 | } 23 | 24 | #[derive(Clone)] 25 | pub struct PeopleModel { 26 | inc: usize, 27 | people: Vec<(Person, IsEditting)>, 28 | on_save: Option>, 29 | link: ComponentLink, 30 | } 31 | 32 | #[derive(Clone, Default, PartialEq, Properties)] 33 | pub struct PeopleProps { 34 | pub on_save: Option>, 35 | } 36 | 37 | #[derive(Clone, Serialize, Deserialize)] 38 | pub struct PeopleStore { 39 | pub inc: usize, 40 | pub people: Vec, 41 | } 42 | 43 | impl PeopleStore { 44 | pub fn restore(local_store: &mut StorageService) -> Option { 45 | let from_store = local_store.restore(PEOPLE_KEY); 46 | match from_store { 47 | Json(Ok(people)) => Some(people), 48 | // TODO: reset local store... 49 | Json(Err(err)) => { 50 | error!("could not load from local store: {}", err); 51 | None 52 | } 53 | } 54 | } 55 | 56 | pub fn store(&mut self, local_store: &mut StorageService) { 57 | self.inc += 1; 58 | debug!("saving people: {}", self.inc); 59 | local_store.store(PEOPLE_KEY, Json(self as &Self)); 60 | } 61 | } 62 | 63 | impl From for PeopleStore { 64 | fn from(model: PeopleModel) -> Self { 65 | Self { 66 | inc: model.inc, 67 | people: model.people.into_iter().map(|(p, _)| p).collect(), 68 | } 69 | } 70 | } 71 | 72 | impl PeopleModel { 73 | fn from( 74 | model: PeopleStore, 75 | on_save: Option>, 76 | link: ComponentLink, 77 | ) -> Self { 78 | Self { 79 | inc: model.inc, 80 | people: model.people.into_iter().map(|p| (p, false)).collect(), 81 | on_save, 82 | link, 83 | } 84 | } 85 | } 86 | 87 | impl Component for PeopleModel { 88 | type Message = PeopleMsg; 89 | type Properties = PeopleProps; 90 | 91 | fn create(props: Self::Properties, link: ComponentLink) -> Self { 92 | debug!("creating PeopleModel"); 93 | 94 | let mut local_store = StorageService::new(Area::Local).expect("failed to get storage"); 95 | 96 | match PeopleStore::restore(&mut local_store) { 97 | Some(this) => Self { 98 | inc: this.inc, 99 | people: this.people.into_iter().map(|p| (p, false)).collect(), 100 | on_save: props.on_save, 101 | link, 102 | }, 103 | None => { 104 | let people = crate::default_people(); 105 | // TODO: make a borrowed type 106 | let mut people = PeopleStore { 107 | inc: 0, 108 | people: people, 109 | }; 110 | 111 | people.store(&mut local_store); 112 | Self { 113 | inc: people.inc, 114 | people: people.people.into_iter().map(|p| (p, false)).collect(), 115 | on_save: props.on_save, 116 | link, 117 | } 118 | } 119 | } 120 | } 121 | 122 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 123 | match msg { 124 | PeopleMsg::SavePeople => { 125 | debug!("saving PeopleModel"); 126 | let mut local_store = 127 | StorageService::new(Area::Local).expect("failed to get storage"); 128 | let mut people: PeopleStore = self.clone().into(); 129 | people.store(&mut local_store); 130 | *self = PeopleModel::from(people, self.on_save.take(), self.link.clone()); 131 | 132 | self.on_save.as_ref().map(|e| e.emit(self.inc)); 133 | true 134 | } 135 | PeopleMsg::AddPerson => { 136 | debug!("adding a Person"); 137 | let person = Person::new("Jane Doe", Ability::Adult); 138 | self.people.push((person, true)); 139 | true 140 | } 141 | PeopleMsg::EditPerson(id) => { 142 | debug!("edit person: {}", id); 143 | self.people 144 | .get_mut(id) 145 | .map(|p| { 146 | if !p.1 { 147 | p.1 = true; 148 | true 149 | } else { 150 | false 151 | } 152 | }) 153 | .unwrap_or(false) 154 | } 155 | PeopleMsg::DeletePerson(idx) => { 156 | let person = self.people.remove(idx); 157 | debug!("deleted {:?}", person); 158 | true 159 | } 160 | PeopleMsg::PersonNameInput(id, name) => self 161 | .people 162 | .get_mut(id) 163 | .map(|p| { 164 | debug!("saving name: {}", name); 165 | if p.0.name() != name { 166 | p.0.set_name(name); 167 | true 168 | } else { 169 | false 170 | } 171 | }) 172 | .unwrap_or(false), 173 | PeopleMsg::PersonAbilityInput(id, ability) => self 174 | .people 175 | .get_mut(id) 176 | .map(|p| { 177 | debug!("saving name: {}", ability); 178 | if p.0.ability() != ability { 179 | p.0.set_ability(ability); 180 | true 181 | } else { 182 | false 183 | } 184 | }) 185 | .unwrap_or(false), 186 | } 187 | } 188 | 189 | fn view(&self) -> Html { 190 | // let select = |is_selected: bool| { 191 | // html!{ 192 | // 193 | // } 194 | // }; 195 | 196 | let edit_delete = |id: Id, is_editting: IsEditting, link: &ComponentLink| { 197 | let on_edit = link.callback(PeopleMsg::EditPerson); 198 | let on_delete = link.callback(PeopleMsg::DeletePerson); 199 | 200 | html! { 201 | 202 | } 203 | }; 204 | let person_row = |id: Id, person: &(Person, IsEditting), link: &ComponentLink| { 205 | let name_on_input = link.callback(|(i, n)| PeopleMsg::PersonNameInput(i, n)); 206 | let ability_on_input = link.callback(|(i, a)| PeopleMsg::PersonAbilityInput(i, a)); 207 | 208 | html! { 209 | 210 | 211 | 212 | { edit_delete(id, person.1, &self.link) } 213 | 214 | } 215 | }; 216 | 217 | html! { 218 | <> 219 |

{"All the beautiful people"}

220 | 221 | 222 | 223 | 224 | 225 | { for self.people.iter().enumerate().map(|(i, p)| person_row(i, p, &self.link)) } 226 | 227 | 228 | 237 | 238 |
{"Person"}{"Ability"}{" "}
229 | 232 | 235 | //button onclick=|_| PeopleMsg::SavePeople, >{"Save all the People"} 236 |
239 | 240 | } 241 | } 242 | } 243 | 244 | // #[derive(Clone, Eq, PartialEq, Default)] 245 | // struct Select { 246 | // is_selected: bool, 247 | // } 248 | 249 | // impl Component for Select { 250 | // type Message = (); 251 | // type Properties = Self; 252 | 253 | // fn create(props: Self::Properties, _context: &mut Env) -> Self { 254 | // Self { 255 | // is_selected: props.is_selected, 256 | // } 257 | // } 258 | 259 | // fn update(&mut self, _msg: Self::Message, _context: &mut Env) -> ShouldRender { 260 | // true 261 | // } 262 | 263 | // fn change( 264 | // &mut self, 265 | // _props: Self::Properties, 266 | // _context: &mut Env, 267 | // ) -> ShouldRender { 268 | // true 269 | // } 270 | // } 271 | 272 | // impl Renderable for Select { 273 | // fn view(&self) -> Html { 274 | // html! { 275 | // 276 | // } 277 | // } 278 | // } 279 | 280 | /// EditDelete Component for a person row 281 | #[derive(Clone)] 282 | struct EditDelete { 283 | id: Id, 284 | is_editting: IsEditting, 285 | on_edit: Option>, 286 | on_delete: Option>, 287 | link: ComponentLink, 288 | } 289 | 290 | #[derive(Clone, PartialEq, Default, Properties)] 291 | struct EditDeleteProps { 292 | pub id: Id, 293 | pub is_editting: IsEditting, 294 | pub on_edit: Option>, 295 | pub on_delete: Option>, 296 | } 297 | 298 | enum EditDeleteMsg { 299 | Edit, 300 | Delete, 301 | } 302 | 303 | impl Component for EditDelete { 304 | type Message = EditDeleteMsg; 305 | type Properties = EditDeleteProps; 306 | 307 | fn create(props: Self::Properties, link: ComponentLink) -> Self { 308 | Self { 309 | id: props.id, 310 | is_editting: props.is_editting, 311 | on_edit: props.on_edit, 312 | on_delete: props.on_delete, 313 | link, 314 | } 315 | } 316 | 317 | fn update(&mut self, msg: Self::Message) -> ShouldRender { 318 | match msg { 319 | EditDeleteMsg::Edit => { 320 | debug!("editting: {}", self.id); 321 | if !self.is_editting { 322 | self.on_edit.as_ref().map(|c| c.emit(self.id)); 323 | } 324 | } 325 | EditDeleteMsg::Delete => { 326 | debug!("deleting: {}", self.id); 327 | self.on_delete.as_ref().map(|c| c.emit(self.id)); 328 | } 329 | } 330 | 331 | false 332 | } 333 | 334 | fn change(&mut self, props: Self::Properties) -> ShouldRender { 335 | if self.is_editting != props.is_editting { 336 | self.is_editting = props.is_editting; 337 | return true; 338 | } 339 | false 340 | } 341 | 342 | fn view(&self) -> Html { 343 | let disabled = if self.is_editting { "disabled" } else { "" }; 344 | 345 | html! { 346 |
347 |