commit a9eaa5dbdef80e57d0fcf259585c1f4679c40e7f Author: Alex Date: Thu Oct 10 18:42:23 2024 +0200 Scaffolding, base structure, base backend diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..1b0b311 Binary files /dev/null and b/favicon.ico differ 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); + })(); + } +}