Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ out/
.project
.settings

# VSC
.vscode

# Python
**/__pycache__/
**/venv/
**/venv/

# Logs
logs
*.log
npm-debug.log*
3 changes: 3 additions & 0 deletions web-components-reuse/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[*.{ts,tsx,js,vue}]
indent_style = space
indent_size = 2
8 changes: 8 additions & 0 deletions web-components-reuse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* as much reuse as possible
* components do not make network calls; they support injecting as much data externally as possible
* avoid ids when to necessary:
* bubbles: true for events
* this.querySelector('[data-{attr}]') pattern for partially updateable/configureable elements
* more explicit deps between components?
* refactor attrs vs props when/where needed which
* dev scoped deps for server!
4 changes: 4 additions & 0 deletions web-components-reuse/components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Define all components in this dir and provide a script to generate a single file with all of them to reuse in various contexts.

TODO: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements
TODO: refactor all components with private, check whether listeners are cleaned up
21 changes: 21 additions & 0 deletions web-components-reuse/components/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"components": [
"base.js",
"tabs-container.js",
"drop-down.js",
"info-modal.js",
"error-modal.js",
"asset-element.js",
"currency-element.js",
"assets-and-currencies.js",
"markets-header.js",
"markets-comparator.js",
"projections-calculator.js",
"markets-projections.js"
],
"outputs": [
"../vue-app/src/components/web-components.js",
"../react-app/src/components/web-components.js",
"../server/assets/web-components.js"
]
}
45 changes: 45 additions & 0 deletions web-components-reuse/components/package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import json
import subprocess
from os import path

with open('config.json') as f:
config = json.load(f)

register_lines = [];
lines_to_write = []
for c in config['components']:
with open(path.join('src', c)) as c:
skip_next_line = False
for c_line in c.readlines():
if 'customElements.define' in c_line:
register_lines.append(c_line)
skip_next_line = True
continue

if skip_next_line:
skip_next_line = False
continue

if 'import' in c_line or 'register()' in c_line:
continue

lines_to_write.append(c_line)


def git_metadata():
commit_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).strip().decode('utf-8')
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip().decode('utf-8')
return f'{branch}:{commit_hash}'


metadata = f'Generated by components/{path.basename(__file__)} from {git_metadata()}'

for o in config['outputs']:
with open(o, "w") as of:
of.write(f"// {metadata}\n\n")
of.write(''.join(lines_to_write))
of.write('\n')
of.write('export function registerComponents() {')
of.write('\n')
of.write(''.join(register_lines))
of.write('}')
64 changes: 64 additions & 0 deletions web-components-reuse/components/src/asset-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { formatMoney, BaseHTMLElement } from './base.js';

class AssetElement extends BaseHTMLElement {

/**
* Supported attributes
* {string} id: asset id
* {string} name: asset name
* {number} market-size
* {number} previous-market-size
* {string} denomination
* {string} class: additional class to append to the root div
*/
connectedCallback() {
this.#render();
}

attributeChangedCallback(name, oldValue, newValue) {
this.#render();
}

#render() {
const [id, name, marketSize, previousMarketSize, denomination] = [this.getAttribute("id"), this.getAttribute("name"),
this.getAttribute("market-size"), this.getAttribute("previous-market-size"), this.getAttribute("denomination")];
if (!id || !name) {
return;
}
const classesToAppend = this.getAttribute("class");

let previousMarketSizeComponent;
if (previousMarketSize && previousMarketSize != marketSize) {
const previousMarketSizeInt = parseInt(previousMarketSize);
const currentMarketSizeInt = parseInt(marketSize);
const marketIsUp = currentMarketSizeInt > previousMarketSizeInt;
let marketPercentageDiff;
if (marketIsUp) {
marketPercentageDiff = Math.round((currentMarketSizeInt - previousMarketSizeInt) * 100 * 100 / previousMarketSizeInt) / 100.0;
} else {
marketPercentageDiff = Math.round((previousMarketSizeInt - currentMarketSizeInt) * 100 * 100 / currentMarketSizeInt) / 100.0;
}
previousMarketSizeComponent = `
<div>
<span class="w-1/2 inline-block">${this.translation("previous-market-size-label")}:</span><span class="underline text-right w-1/2 inline-block">${formatMoney(previousMarketSize, denomination)}</span>
<div>
<p class="text-right italic">${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%</p>`;
} else {
previousMarketSizeComponent = ``;
}

this.innerHTML = `
<div data-id=${id} class="border-1 p-2 rounded-lg ${classesToAppend ? classesToAppend : ""}">
<p class="font-bold">${name}</p>
<div>
<span class="w-1/2 inline-block">${this.translation('market-size-label')}:</span><span class="underline text-right w-1/2 inline-block">${formatMoney(marketSize, denomination)}</span>
</div>
${previousMarketSizeComponent}
</div>
`;
}
}

export function register() {
customElements.define("asset-element", AssetElement);
}
146 changes: 146 additions & 0 deletions web-components-reuse/components/src/assets-and-currencies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { BaseHTMLElement } from "./base.js";

/**
* @typedef {Object} AssetOrCurrencyElement
* @property {string} id
* @property {string} name
* @property {number} marketSize
* @property {string} denomination
*/

class AssetsAndCurrencies extends BaseHTMLElement {

#assets = [];
#assetsValueChangeReason = null;
#currencies = [];
#denomination = "USD";
#assetsContainer = null;
#currenciesContainer = null;

/** @type {AssetOrCurrencyElement[]} */
set assets(value) {
this.#assets = value;
this.#renderAssets();
}

set assetsValueChangeReason(value) {
this.#assetsValueChangeReason = value;
this.#renderAssets();
}

/** @type {AssetOrCurrencyElement[]} */
set currencies(value) {
this.#currencies = value;
this.#renderCurrencies();
}

/** @type {string} */
set denomination(value) {
this.#denomination = value;
this.#renderAssets();
this.#renderCurrencies();
}

connectedCallback() {
this.innerHTML = `
<div class="m-4">
<tabs-container active-tab-class="underline">
<div class="flex" data-tabs-header>
<tab-header>${this.translation('assets-header')}</tab-header>
<tab-header>${this.translation('currencies-header')}</tab-header>
</div>
<div data-tabs-body>
<div class="h-[40dvh] overflow-y-auto">
${this.#assetsHTML()}
</div>
<div class="h-[40dvh] overflow-y-auto">
${this.#currenciesHTML()}
</div>
</div>
</tabs-container>
</div>`;

const tabsBody = this.querySelector("[data-tabs-body]");
this.#assetsContainer = tabsBody.children[0];
this.#currenciesContainer = tabsBody.children[1];
}

#assetsHTML(previousAssetElements = []) {
return this.#assets.map(a => {
const previousAsset = previousAssetElements.find(pa => pa.id == a.id);
let previousMarketSize;
if (!previousAsset) {
previousMarketSize = a.marketSize;
} else {
const previousAssetDenomination = previousAsset.getAttribute("denomination");
const previousAssetMarketSize = previousAsset.getAttribute("market-size");
// if denomination has changed, comparing current market size with the previous is meaningless
if (previousAssetDenomination != a.denomination) {
previousMarketSize = a.marketSize;
} else if (previousAssetMarketSize != a.marketSize) {
previousMarketSize = previousAssetMarketSize;
} else {
previousMarketSize = previousAsset.getAttribute("previous-market-size");
}
}

return `<asset-element class="my-2" id="${a.id}" name="${a.name}"
market-size="${a.marketSize}" previous-market-size="${previousMarketSize}"
denomination="${a.denomination}"
value-change-reason="${this.#assetsValueChangeReason}"
${this.translationAttribute('market-size-label')}
${this.translationAttribute('previous-market-size-label')}
${this.translationAttribute('up-by-info')}
${this.translationAttribute('down-by-info')}>
</asset-element>`;
}).join("\n");
}

#currenciesHTML(previousCurrencyElements = []) {
return this.#currencies.map(c => {
const previousCurrency = previousCurrencyElements.find(pc => pc.id == c.id);
let previousMarketSize;
if (!previousCurrency) {
previousMarketSize = c.marketSize;
} else {
const previousCurrencyDenomination = previousCurrency.getAttribute("denomination");
const previousCurrencyMarketSize = previousCurrency.getAttribute("market-size");
// if denomination has changed, comparing current market size with the previous is meaningless
if (previousCurrencyDenomination != c.denomination) {
previousMarketSize = c.marketSize;
} else if (previousCurrencyMarketSize != c.marketSize) {
previousMarketSize = previousCurrencyMarketSize;
} else {
previousMarketSize = previousCurrency.getAttribute("previous-market-size");
}
}

return `<currency-element class="my-2" id="${c.id}" name="${c.name}"
market-size="${c.marketSize}" previous-market-size="${previousMarketSize}"
denomination="${c.denomination}"
${this.translationAttribute('daily-turnover-label')}
${this.translationAttribute('yearly-turnover-label')}
${this.translationAttribute('up-by-info')}
${this.translationAttribute('down-by-info')}>
</currency-element>`})
.join("\n");
}

#renderAssets() {
if (this.#assetsContainer) {
const currentAssetElements = [...this.querySelectorAll("asset-element")];
this.#assetsContainer.innerHTML = this.#assetsHTML(currentAssetElements);
}
}

#renderCurrencies() {
if (this.#currenciesContainer) {
const currentCurrencyElements = [...this.querySelectorAll("currency-element")];
this.#currenciesContainer.innerHTML = this.#currenciesHTML(currentCurrencyElements);
}
}
}

export function register() {
customElements.define('assets-and-currencies', AssetsAndCurrencies);
}
63 changes: 63 additions & 0 deletions web-components-reuse/components/src/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Common types definition
/**
* @typedef {Object} AssetOrCurrency
* @property {string} name
* @property {number} marketSize
*/

export function formatMoney(value, denomination) {
const zeros = value.length;
if (zeros > 15) {
return `${value.substring(0, zeros - 15)} ${value.substring(zeros - 15, zeros - 12)} ${value.substring(zeros - 12, zeros - 9)} ${value.substring(zeros - 9, zeros - 6)} ${value.substring(zeros - 6, zeros - 3)} ${value.substring(zeros - 3)} ${denomination}`;
}
if (zeros > 12) {
return `${value.substring(0, zeros - 12)} ${value.substring(zeros - 12, zeros - 9)} ${value.substring(zeros - 9, zeros - 6)} ${value.substring(zeros - 6, zeros - 3)} ${value.substring(zeros - 3)} ${denomination}`;
}
if (zeros > 9) {
return `${value.substring(0, zeros - 9)} ${value.substring(zeros - 9, zeros - 6)} ${value.substring(zeros - 6, zeros - 3)} ${value.substring(zeros - 3)} ${denomination}`;
}
if (zeros > 6) {
return `${value.substring(0, zeros - 6)} ${value.substring(zeros - 6, zeros - 3)} ${value.substring(zeros - 3)} ${denomination}`;
}
return `${value} ${denomination}`;
}

export class BaseHTMLElement extends HTMLElement {

_t = null;
_tNamespace = '';

set t(value) {
this._t = value;
// TODO: shouldn't all components be re-renderable and called from here?
}

set tNamespace(value) {
this._tNamespace = value;
}

translation(key) {
const namespacedKey = (this.getAttribute("t-namespace") ?? this._tNamespace) + key;
const attributeTranslation = this.getAttribute(`t-${namespacedKey}`);
if (attributeTranslation != undefined) {
return attributeTranslation;
}
return this._t ? this._t(namespacedKey) : null;
}

translationAttribute(key) {
const translation = this.translation(key);
if (translation) {
return `t-${key}="${translation}"`;
}
return "";
}

translationAttributeRemovingNamespace(key, namespace) {
const translation = this.translation(namespace + key);
if (translation) {
return `t-${key}="${translation}"`;
}
return "";
}
}
Loading