From a9eaa5dbdef80e57d0fcf259585c1f4679c40e7f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 10 Oct 2024 18:42:23 +0200 Subject: [PATCH] Scaffolding, base structure, base backend --- favicon.ico | Bin 0 -> 11454 bytes index.html | 15 ++++ serve.sh | 3 + src/components/app-base.js | 52 ++++++++++++++ src/components/custom-button.js | 60 ++++++++++++++++ src/components/item-list.js | 45 ++++++++++++ src/components/main-window.js | 83 +++++++++++++++++++++ src/main.js | 34 +++++++++ src/modules/store.js | 95 ++++++++++++++++++++++++ src/modules/text.js | 4 ++ src/style.css | 0 test/index.html | 17 +++++ test/main.js | 50 +++++++++++++ test/modules/store.spec.js | 124 ++++++++++++++++++++++++++++++++ 14 files changed, 582 insertions(+) create mode 100644 favicon.ico create mode 100644 index.html create mode 100755 serve.sh create mode 100644 src/components/app-base.js create mode 100644 src/components/custom-button.js create mode 100644 src/components/item-list.js create mode 100644 src/components/main-window.js create mode 100644 src/main.js create mode 100644 src/modules/store.js create mode 100644 src/modules/text.js create mode 100644 src/style.css create mode 100644 test/index.html create mode 100644 test/main.js create mode 100644 test/modules/store.spec.js diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b0b31157d0ea37eba7ae83ab39f998f3a047059 GIT binary patch literal 11454 zcmeI23sh8f9>>3K+A8DFFn|Ih4u&suL=yqS(9{M<7RSxBQc1S5@L86oZl$i7k2YN& z)oL$$toT?hb=xeVe5RF^rfELInE_@PhL=1p7w-Ss?|)}NFj$Av*>iT!x!3cV|God; z{eS_}*>7~oIbM(C`#gA+jLtr)1cVPK#Q zF~lHV1`)w@PQ-Mgl8EJHXB*;(*bpL=&N*Q=#1nA_Dv80o>_R0c+=he*8-{eVAu-a9 z#O`(s>uJZ`y@=j+3@1kPwPPg5WXC=I>@Y_Y{q0C1QU>NBH7*ZncM=JCunft=n7fF? zJlr=dANP;Q$GDOCc)&~~<>R50d`uwH(+ZF=rT~wOEx=>r3NU3n$AKpvbRctr15+nD zFl~|pGahze=42wnfmy^;k2*1%czTKxb26QnJJpG2r#UfidLib|D8z!9g?R4CLOega z5R0C1VbL5H7SDBI$vhWcoKGxpVJWd#K^fVNEeMa@H2(&2_~3V!ZuY2{vsg!Fz9%;CYKP{7xyd zHkD$_yQSE%xePnAh%IIKkoag@8FpvR0mWCR0mWCR0mWC zR0mWCR0mWC{!bhbMX|+9|5L2RM%M!(W3pUcT0wt2u07E4-8o6jgH-!qD;4LLX0m)P_A#_C+nJWf{fVQ2fA znyB*&!{@Ehm##h%Suc^A;_WDDlA_O!c1ZTd%yEO_lRW22(!S;|R!$n^X*M{l<6UEW zYJzVk$)tz5KKtg@G$oeSOFQ{5FTIfNr*z*~Kl^6>$g>705@!EDA1RY*8lHOZkDv|L zDf&EVuRk}*UlhWX6bDTc8$BpvuRJ6R>`;IrAG}enpX72l_sihBKKrl>*D#M2>m`(F zBg!<#<;Rr;5w$`YYNcN8+)90U%1Evs@Li+qJrh%qF8K3MO0suHAm5m*<5j-rFF7^5 z!SHFva@r-d6n<_>y$3pJe|Ry znAsZre?)MNkQ2@5%j5XcN`AomXl$X<9lyO5`M;*De!acP=qmXNER2t3;#0p#|MfmR zq2~2Fji%7hS-(l&Gln5QH0RH~DWpGYM7{oeeYp3ypPx^v)ed)jY^#e$k zv^@XR-p#-BebZ-KyK<4F`)+tqOWfhdl>MR4${Qb!@fG2lzE~Q-XQAbe*^N7pm}@l? z*nT|^Spwge>%SRDw}#0_@~H#g^ab{TvVtv9nFZcmQe?9GE$#i3v^-rIVN2lhYH#m+ z^FEM?3%-so<-`8u5gDpob)LR`WVtx!1#P4`o%E$Vjq-37Q$&`-^M3hy{gBY%f&8Py z$NUA!W%PV|j2@uxK6mkgi5dfK1W6OQ=`82>ibIyv2;4KdN8&|hwazQ8U9 zDtk03r6$BDc;4a34=RHy?`Y?F0_rh-V5~CUq@UXweS!TvtcNGf>%Yn46@5WIsB75P z8lrk@tMmmX{MGNyhSvNf)%XXGzQCMM>xMRoSw{Y@mHPAncV>>CF_G_Q^ukK$5gf6x zfKpTrNj^GN8(Ht5Il7DH(JVe)Wn-ktWYX;Y^}_TvY*H{eZ|jq{`RnunntSKI^L3LC zTqa-K^mzM#prF8j_8DvbO*4r%Wt-N%`s%vB{aBwz6S-zCB$}EfPMf`)f1uYiy@6uP zcJAD{JpO@RFX-8e>aUtMXyHnfB&pF-ZG0CGwHR!Cqk677pgN#BpgN#BpgN#BpgN#B ZpgQnB<3P**FHnuB4yX>O4*Yi=_yxw`&kz6r literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..984b921 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + Prueba CGI + + + + +
+ + + diff --git a/serve.sh b/serve.sh new file mode 100755 index 0000000..20e6347 --- /dev/null +++ b/serve.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +npx http-server . diff --git a/src/components/app-base.js b/src/components/app-base.js new file mode 100644 index 0000000..3acc503 --- /dev/null +++ b/src/components/app-base.js @@ -0,0 +1,52 @@ +import Store from '../modules/store.js'; +import text from '../modules/text.js'; + +class AppBase extends HTMLElement { + static observedAttributes = ['title', 'description']; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.text = text; + this.store = new Store(); + } + + connectedCallback() { + this.store.subscribe(this.render.bind(this)); + this.render(); + + console.log('Component constructed', this); + } + + disconnectedCallback() { + console.log('Component disconnected', this); + } + + attributeChangedCallback() { + this.render(); + } + + renderText() { + const textList = this.store.getRecords(); + const selected = this.store.getSelected(); + return textList.map((item) => ``).join('\n'); + } + + render() { + this.shadowRoot.innerHTML = ` + + + + Delete + Add + Undo + + + `; + } +} + +customElements.define('app-base', AppBase); + diff --git a/src/components/custom-button.js b/src/components/custom-button.js new file mode 100644 index 0000000..966bd3d --- /dev/null +++ b/src/components/custom-button.js @@ -0,0 +1,60 @@ +import Store from '../modules/store.js'; + +class CustomButton extends HTMLElement { + static observedAttributes = ['outline', 'color', 'action']; + + constructor() { + super(); + this.publish = Store.publish; + this.attachShadow({ mode: 'open' }); + } + + onClick(e) { + this.publish(this.getAttribute('action')); + } + + connectedCallback() { + this.render(); + this.shadowRoot.getElementById('button').addEventListener('click', this.onClick.bind(this)); + + console.log('Component constructed', this); + } + + disconnectedCallback() { + this.shadowRoot.getElementById('button').removeEventListener('click', this.onClick); + + console.log('Component disconnected', this); + } + + attributeChangedCallback() { + this.render(); + } + + render() { + const isOutline = this.getAttribute('outline') !== null && this.getAttribute('outline') !== 'false'; + this.shadowRoot.innerHTML = ` + + + `; + } +} + +customElements.define('custom-button', CustomButton); diff --git a/src/components/item-list.js b/src/components/item-list.js new file mode 100644 index 0000000..d09f6a1 --- /dev/null +++ b/src/components/item-list.js @@ -0,0 +1,45 @@ +import Store from '../modules/store.js'; + +class ItemList extends HTMLSelectElement { + constructor() { + super(); + this.publish = Store.publish + } + + connectedCallback() { + this.addEventListener('click', this.onChange.bind(this)); + this.updateStyle(); + + console.log('Component constructed', this); + } + + onChange() { + const selectedIds = Array.from(this.selectedOptions).map((option) => option.value); + this.publish(Store.actions.SELECT, selectedIds); + } + + disconnectedCallback() { + this.removeEventListener('change', this.onChange.bind(this)); + + console.log('Component disconnected', this); + } + + attributeChangedCallback() { + this.updateStyle(); + } + + updateStyle() { + this.style.width = '800px'; + this.style.height = '227px'; + this.style.background = '#F7F7F7 0% 0% no-repeat padding-box'; + this.style.border = '1px solid #CCCCCC'; + this.style.opacity = '1'; + this.style.fontSize = '18px'; + this.style.fontFamily = '"Montserrat" sans-serif'; + this.style.fontWeight = 'normal'; + this.style.letterSpacing = '0px'; + } +} + +customElements.define('item-list', ItemList, { extends: 'select' }); + diff --git a/src/components/main-window.js b/src/components/main-window.js new file mode 100644 index 0000000..90cb909 --- /dev/null +++ b/src/components/main-window.js @@ -0,0 +1,83 @@ +class MainWindow extends HTMLElement { + static observedAttributes = ['title', 'description']; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + console.log('Component constructed', this); + } + + disconnectedCallback() { + console.log('Component disconnected', this); + } + + attributeChangedCallback() { + this.render(); + } + + render() { + const title = this.getAttribute('title') || "No title"; + const description = this.getAttribute('description') || "No description"; + + this.shadowRoot.innerHTML = ` + +
+

${title}

+

${description}

+ +
+ `; + } +} + +customElements.define('main-window', MainWindow); + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..3558e36 --- /dev/null +++ b/src/main.js @@ -0,0 +1,34 @@ +import './components/app-base.js'; +import './components/main-window.js'; +import './components/custom-button.js'; +import './components/item-list.js'; + +class App { + constructor(root, text, store) { + this.root = root; + } + + start() { + this.root.innerHTML = ` + + + `; + } +} + +const app = new App(document.getElementById('root')); + +app.start(); + diff --git a/src/modules/store.js b/src/modules/store.js new file mode 100644 index 0000000..13e0e2d --- /dev/null +++ b/src/modules/store.js @@ -0,0 +1,95 @@ +export default class Store { + static actions = { + ADD_LINE: 'ADD_LINE', + REMOVE_LINE: 'REMOVE_LINE', + UNDO: 'UNDO', + SELECT: 'SELECT', + } + + constructor() { + this.state = { + records: [], + selected: [], + }; + this.callbacks = []; + + Object.values(this.constructor.actions).forEach(action => document.addEventListener(action, this.eventHandler.bind(this), false)); + } + + subscribe(callback) { + this.callbacks = [ ...this.callbacks, callback ]; + } + + static publish(action, data = {}) { + if (!Object.values(Store.actions).includes(action)) { + console.error(`Action ${action} is not valid`); + return; + } + + const event = new CustomEvent(action, { detail: data }); + document.dispatchEvent(event); + } + + eventHandler(event) { + if (!Object.values(this.constructor.actions).includes(event.type)) { + console.error(`Action "${event.type}" is not valid`); + return; + } + + switch (event.type) { + case this.constructor.actions.ADD_LINE: + this.state = { + ...this.state, + records: this.state.records.concat({ + id: this.state.records.length, + text: 'asdf', + action: 'add', + }), + }; + break; + case this.constructor.actions.REMOVE_LINE: + this.state = { + ...this.state, + records: this.records.concat({ + id: this.state.records.length, + action: 'remove', + }), + }; + break; + case this.constructor.actions.UNDO: + this.state = { + ...this.state, + records: this.state.records.slice(0, -1), + } + break; + case this.constructor.actions.SELECT: + this.state = { + ...this.state, + selected: event.detail, + } + break; + default: + console.error(`Action "${event.type}" not implemented in eventHandler`); + return; + } + + this.callbacks.forEach((cb) => cb()); + } + + getRecords() { + return this.state.records.reduce((acc, current) => { + if (current.action === "add") { + return acc.concat({id: current.id, text: current.text}); + } + if (current.action === "remove") { + return acc.filter((item) => item.id !== current.targetId); + } + console.error(`Action ${current.action} not valid`); + }, []); + } + + getSelected() { + return this.state.selected; + } + +} diff --git a/src/modules/text.js b/src/modules/text.js new file mode 100644 index 0000000..9b679af --- /dev/null +++ b/src/modules/text.js @@ -0,0 +1,4 @@ +export default { + title: 'This is a technical proof', + description: 'Lorem ipsum dolor sit amet consectetur adipiscing, elit mus primis nec inceptos. Lacinia habitasse arcu molestie maecenas cursus quam nunc, hendrerit posuere augue fames dictumst placerat porttitor, dis mi pharetra vestibulum venenatis phasellus.', +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..e69de29 diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..fa13696 --- /dev/null +++ b/test/index.html @@ -0,0 +1,17 @@ + + + + + + + Test suite + + + +
+Test suite
+
+
+ + + diff --git a/test/main.js b/test/main.js new file mode 100644 index 0000000..058322c --- /dev/null +++ b/test/main.js @@ -0,0 +1,50 @@ +import StoreTest from './modules/store.spec.js'; + +class Test { + constructor(root) { + this.root = root; + this.tests = []; + this.pass = 0 + this.fail = 0 + } + + start() { + this.defineTests(); + this.tests.forEach((testClass) => { + this.log(`Testing ${testClass.constructor.name}`); + testClass.test(); + } + ); + this.log(` + +Passed ${this.pass} tests +Failed ${this.fail} tests + `); + } + + defineTests() { + this.tests = [ + new StoreTest(this.assert.bind(this)), + ]; + } + + assert(description, current, expected) { + const sameType = typeof current === typeof expected; + const sameValue = JSON.stringify(current) === JSON.stringify(expected); + let result = '[ERROR]'; + if (sameType && sameValue) { + this.log(` [PASS] ${description}`); + this.pass++; + } else { + this.log(` [FAIL] ${description}. Expected (${typeof expected}) ${JSON.stringify(expected)}, got (${typeof current}) ${JSON.stringify(current)}`); + this.fail++; + } + } + + log(text) { + this.root.insertAdjacentText("beforeend", `${text}\n`); + } +} + +const testSuite = new Test(document.getElementById('root')); +testSuite.start(); diff --git a/test/modules/store.spec.js b/test/modules/store.spec.js new file mode 100644 index 0000000..0da6eb3 --- /dev/null +++ b/test/modules/store.spec.js @@ -0,0 +1,124 @@ +import Store from '../../src/modules/store.js'; + +export default class StoreTest { + constructor(assert) { + this.assert = assert; + } + + test() { + (() => { + let called = false + const store = new Store(() => called = true); + + const event = new Event('addLine'); + store.eventHandler(event); + + this.assert("eventHandler calls the callback after successfull event", called, true); + + })(); + (() => { + const store = new Store(() => {}); + store.records = [ + {id: 0, text: "asdf", action: 'add'}, + {id: 1, text: "qwer", action: 'add'}, + {id: 2, action: 'remove'}, + ] + + const event = new Event('unknown'); + store.eventHandler(event); + + const expected = [ + {id: 0, text: "asdf", action: 'add'}, + {id: 1, text: "qwer", action: 'add'}, + {id: 2, action: 'remove'}, + ]; + + this.assert("eventHandler does nothing on an unknown event type", store.records, expected); + + })(); + (() => { + const store = new Store(() => {}); + + const event = new Event('addLine'); + store.eventHandler(event); + + const expected = [ + {id: 0, text: "asdf", action: 'add'}, + ]; + + this.assert("eventHandler adds the appropiate record when Event 'addLine' is passed", store.records, expected); + + })(); + (() => { + const store = new Store(() => {}); + store.records = [ + {id: 0, text: "asdf", action: 'add'}, + ] + + const event = new Event('removeLine'); + store.eventHandler(event); + + const expected = [ + {id: 0, text: "asdf", action: 'add'}, + {id: 1, action: 'remove'}, + ]; + + this.assert("eventHandler adds the appropiate record when Event 'removeLine' is passed", store.records, expected); + + })(); + (() => { + const store = new Store(() => {}); + store.records = [ + {id: 0, text: "asdf", action: 'add'}, + {id: 1, text: "qwer", action: 'add'}, + {id: 2, action: 'remove'}, + ] + + const event = new Event('undo'); + store.eventHandler(event); + + const expected = [ + {id: 0, text: "asdf", action: 'add'}, + {id: 1, text: "qwer", action: 'add'}, + ]; + + this.assert("eventHandler deletes the appropiate record when Event 'undo' is passed", store.records, expected); + + })(); + (() => { + const store = new Store(() => {}); + + store.records = [ + {id: 0, text: "Line 1", action: 'add'}, + {id: 1, text: "Line 2", action: 'add'}, + {id: 2, text: "Line 3", action: 'add'}, + ]; + + const expected = [ + {id: 0, text: "Line 1"}, + {id: 1, text: "Line 2"}, + {id: 2, text: "Line 3"}, + ]; + + this.assert("getState returns an array of strings with 3 add records", store.getState(), expected); + })(); + + (() => { + const store = new Store(() => {}); + + store.records = [ + {id: 0, text: "Line 1", action: 'add'}, + {id: 1, text: "Line 2", action: 'add'}, + {id: 2, text: "Line 3", action: 'add'}, + {id: 3, targetId: 1, action: 'remove'}, + ]; + + const expected = [ + {id: 0, text: "Line 1"}, + {id: 2, text: "Line 3"}, + ]; + + this.assert("getState returns an array of strings with 3 add records and a delete record", store.getState(), expected); + })(); + } +}