From 3e2477d5554de95b89cd048a00f5d452883a34c2 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 13 Sep 2025 07:49:19 +0200 Subject: [PATCH 01/13] WebComponents: vue --- .gitignore | 3 + web-components-reuse/.editorconfig | 3 + web-components-reuse/vue-app/.gitignore | 24 + web-components-reuse/vue-app/README.md | 5 + web-components-reuse/vue-app/index.html | 13 + .../vue-app/package-lock.json | 2074 +++++++++++++++++ web-components-reuse/vue-app/package.json | 24 + web-components-reuse/vue-app/public/vite.svg | 1 + web-components-reuse/vue-app/src/App.vue | 9 + .../vue-app/src/assets/vue.svg | 1 + .../vue-app/src/components/Home.vue | 134 ++ .../src/components/MarketsComparator.vue | 53 + .../src/components/MarketsComparatorInput.vue | 75 + .../src/components/ProjectionsCalculator.vue | 67 + .../src/components/ProjectionsResult.vue | 68 + .../src/components/lib/asset-element.js | 63 + .../src/components/lib/currency-element.js | 62 + .../src/components/lib/customizations.js | 13 + .../vue-app/src/components/lib/drop-down.js | 69 + .../lib/markets-comparator-input.js | 33 + .../vue-app/src/components/lib/registry.js | 16 + .../src/components/lib/tabs-container.js | 58 + .../vue-app/src/components/lib/utils.js | 16 + web-components-reuse/vue-app/src/data/api.ts | 35 + .../vue-app/src/data/currency-code.ts | 36 + .../vue-app/src/data/mocked-api.ts | 197 ++ .../vue-app/src/data/updater.ts | 60 + web-components-reuse/vue-app/src/main.ts | 21 + web-components-reuse/vue-app/src/style.css | 1 + .../vue-app/src/vite-env.d.ts | 1 + .../vue-app/tsconfig.app.json | 16 + web-components-reuse/vue-app/tsconfig.json | 7 + .../vue-app/tsconfig.node.json | 24 + web-components-reuse/vue-app/vite.config.ts | 14 + 34 files changed, 3296 insertions(+) create mode 100644 web-components-reuse/.editorconfig create mode 100644 web-components-reuse/vue-app/.gitignore create mode 100644 web-components-reuse/vue-app/README.md create mode 100644 web-components-reuse/vue-app/index.html create mode 100644 web-components-reuse/vue-app/package-lock.json create mode 100644 web-components-reuse/vue-app/package.json create mode 100644 web-components-reuse/vue-app/public/vite.svg create mode 100644 web-components-reuse/vue-app/src/App.vue create mode 100644 web-components-reuse/vue-app/src/assets/vue.svg create mode 100644 web-components-reuse/vue-app/src/components/Home.vue create mode 100644 web-components-reuse/vue-app/src/components/MarketsComparator.vue create mode 100644 web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue create mode 100644 web-components-reuse/vue-app/src/components/ProjectionsCalculator.vue create mode 100644 web-components-reuse/vue-app/src/components/ProjectionsResult.vue create mode 100644 web-components-reuse/vue-app/src/components/lib/asset-element.js create mode 100644 web-components-reuse/vue-app/src/components/lib/currency-element.js create mode 100644 web-components-reuse/vue-app/src/components/lib/customizations.js create mode 100644 web-components-reuse/vue-app/src/components/lib/drop-down.js create mode 100644 web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js create mode 100644 web-components-reuse/vue-app/src/components/lib/registry.js create mode 100644 web-components-reuse/vue-app/src/components/lib/tabs-container.js create mode 100644 web-components-reuse/vue-app/src/components/lib/utils.js create mode 100644 web-components-reuse/vue-app/src/data/api.ts create mode 100644 web-components-reuse/vue-app/src/data/currency-code.ts create mode 100644 web-components-reuse/vue-app/src/data/mocked-api.ts create mode 100644 web-components-reuse/vue-app/src/data/updater.ts create mode 100644 web-components-reuse/vue-app/src/main.ts create mode 100644 web-components-reuse/vue-app/src/style.css create mode 100644 web-components-reuse/vue-app/src/vite-env.d.ts create mode 100644 web-components-reuse/vue-app/tsconfig.app.json create mode 100644 web-components-reuse/vue-app/tsconfig.json create mode 100644 web-components-reuse/vue-app/tsconfig.node.json create mode 100644 web-components-reuse/vue-app/vite.config.ts diff --git a/.gitignore b/.gitignore index de4e3dcc..a10bd926 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ out/ .project .settings +# VSC +.vscode + # Python **/__pycache__/ **/venv/ \ No newline at end of file diff --git a/web-components-reuse/.editorconfig b/web-components-reuse/.editorconfig new file mode 100644 index 00000000..cb7baab8 --- /dev/null +++ b/web-components-reuse/.editorconfig @@ -0,0 +1,3 @@ +[*.{js,jsx,ts,tsx}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/web-components-reuse/vue-app/.gitignore b/web-components-reuse/vue-app/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/web-components-reuse/vue-app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web-components-reuse/vue-app/README.md b/web-components-reuse/vue-app/README.md new file mode 100644 index 00000000..33895ab2 --- /dev/null +++ b/web-components-reuse/vue-app/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/web-components-reuse/vue-app/package-lock.json b/web-components-reuse/vue-app/package-lock.json new file mode 100644 index 00000000..d6e56cb6 --- /dev/null +++ b/web-components-reuse/vue-app/package-lock.json @@ -0,0 +1,2074 @@ +{ + "name": "vue-app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vue-app", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.13", + "tailwindcss": "^4.1.13", + "vue": "^3.5.18", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.29", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", + "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", + "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.29" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz", + "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz", + "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", + "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.21", + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.18", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz", + "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.0.7.tgz", + "integrity": "sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^2.0.5", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.21.tgz", + "integrity": "sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.21.tgz", + "integrity": "sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/shared": "3.5.21" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.21.tgz", + "integrity": "sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.21", + "@vue/runtime-core": "3.5.21", + "@vue/shared": "3.5.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.21.tgz", + "integrity": "sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "vue": "3.5.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz", + "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-2.0.7.tgz", + "integrity": "sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", + "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.21", + "@vue/compiler-sfc": "3.5.21", + "@vue/runtime-dom": "3.5.21", + "@vue/server-renderer": "3.5.21", + "@vue/shared": "3.5.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.0.7.tgz", + "integrity": "sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.0.7" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/web-components-reuse/vue-app/package.json b/web-components-reuse/vue-app/package.json new file mode 100644 index 00000000..9a7b0e1d --- /dev/null +++ b/web-components-reuse/vue-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "vue-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.13", + "tailwindcss": "^4.1.13", + "vue": "^3.5.18", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "~5.8.3", + "vite": "^7.1.2", + "vue-tsc": "^3.0.5" + } +} diff --git a/web-components-reuse/vue-app/public/vite.svg b/web-components-reuse/vue-app/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/web-components-reuse/vue-app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/App.vue b/web-components-reuse/vue-app/src/App.vue new file mode 100644 index 00000000..fce3e5a8 --- /dev/null +++ b/web-components-reuse/vue-app/src/App.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/web-components-reuse/vue-app/src/assets/vue.svg b/web-components-reuse/vue-app/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/web-components-reuse/vue-app/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/Home.vue b/web-components-reuse/vue-app/src/components/Home.vue new file mode 100644 index 00000000..f11b9a28 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/Home.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web-components-reuse/vue-app/src/components/MarketsComparator.vue b/web-components-reuse/vue-app/src/components/MarketsComparator.vue new file mode 100644 index 00000000..c55c73c2 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/MarketsComparator.vue @@ -0,0 +1,53 @@ + + + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue b/web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue new file mode 100644 index 00000000..bab99cf4 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue @@ -0,0 +1,75 @@ + + + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/ProjectionsCalculator.vue b/web-components-reuse/vue-app/src/components/ProjectionsCalculator.vue new file mode 100644 index 00000000..bc348de0 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/ProjectionsCalculator.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/ProjectionsResult.vue b/web-components-reuse/vue-app/src/components/ProjectionsResult.vue new file mode 100644 index 00000000..11a3d18c --- /dev/null +++ b/web-components-reuse/vue-app/src/components/ProjectionsResult.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/asset-element.js b/web-components-reuse/vue-app/src/components/lib/asset-element.js new file mode 100644 index 00000000..c8ca3687 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/asset-element.js @@ -0,0 +1,63 @@ +// TODO: better styling & translations +import * as Utils from './utils.js'; + +class AssetElement extends HTMLElement { + + static observedAttributes = ["market-size", "denomination", "value-change-reasons"]; + _previousMarketSize = null; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + this._render(); + if (name == 'market-size') { + this._previousMarketSize = newValue; + } + } + + _render() { + const [id, name, marketSize, denomination, valueChangeReasons] = [this.getAttribute("id"), this.getAttribute("name"), + this.getAttribute("market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reasons") + ]; + if (id == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +
+ Previous market size:${Utils.formatMoney(this._previousMarketSize, denomination)} +
+

${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%; ${valueChangeReasons ?? 'UNKNOWN'}

`; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
+

${name}

+
+ Market size:${Utils.formatMoney(marketSize, denomination)} +
+ ${previousMarketSizeComponent} +
+ `; + } +} + +export function register() { + customElements.define("asset-element", AssetElement); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/currency-element.js b/web-components-reuse/vue-app/src/components/lib/currency-element.js new file mode 100644 index 00000000..bafc5524 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/currency-element.js @@ -0,0 +1,62 @@ +// TODO: better styling & translations +import * as Utils from './utils.js'; + +class CurrencyElement extends HTMLElement { + + static observedAttributes = ["market-size", "denomination"]; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + this._render(); + if (name == 'market-size') { + this._previousMarketSize = newValue; + } + } + + _render() { + const [id, name, marketSize, denomination] = [this.getAttribute("id"), this.getAttribute("name"), + this.getAttribute("market-size"), this.getAttribute("denomination") + ]; + if (id == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +

${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%

`; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
+

${name}

+
+ Daily turnover:${Utils.formatMoney(marketSize, denomination)} +
+
+ Yearly turnover:${Utils.formatMoney(`${365 * parseInt(marketSize)}`, denomination)} +
+ ${previousMarketSizeComponent} +
+ `; + } +} + +export function register() { + customElements.define("currency-element", CurrencyElement); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/customizations.js b/web-components-reuse/vue-app/src/components/lib/customizations.js new file mode 100644 index 00000000..94d1367d --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/customizations.js @@ -0,0 +1,13 @@ +class TabHeader extends HTMLElement { + + connectedCallback() { + this.classList.add("text-2xl"); + this.classList.add("p-2"); + this.classList.add("cursor-pointer"); + this.classList.add("grow"); + } +} + +export function register() { + customElements.define('tab-header', TabHeader); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/drop-down.js b/web-components-reuse/vue-app/src/components/lib/drop-down.js new file mode 100644 index 00000000..5d875ac2 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/drop-down.js @@ -0,0 +1,69 @@ +class DropDownContainer extends HTMLElement { + + connectedCallback() { + const anchor = this.querySelector('[data-drop-down-anchor]') ?? this; + anchor.style = "position: relative; display: inline-block"; + + const options = this.querySelector("[data-drop-down-options]"); + if (!options) { + throw new Error("Options must be defined and marked with data-drop-down-options attribute!"); + } + options.style = "position: absolute; z-index: 99"; + options.classList.add("hidden"); + + anchor.onclick = (e) => { + // Do not hide other, opened DropDowns + e.stopPropagation(); + options.classList.toggle("hidden"); + }; + + window.addEventListener("click", e => { + if (e.target != anchor && e.target.parentNode != anchor) { + console.log("Global DropDown close!"); + options.classList.add("hidden"); + } + }); + } +} + +class DropDown extends HTMLElement { + + connectedCallback() { + const title = this.getAttribute("title"); + + // 3 guys: + // + //
Title
+ // + //
  • + //
  • + //
    + //
    + // + + this.innerHTML = ` +
    +
    ${title}
    + +
    + `; + + const options = this.querySelector("ul"); + this._optionsElement = options; + + const container = this.querySelector("div"); + container.onclick = (e) => { + // Do not hide other, opened DropDowns + e.stopPropagation(); + options.classList.toggle("hidden"); + }; + } +} + +export function register() { + customElements.define("drop-down-container", DropDownContainer); + customElements.define("drop-down", DropDown); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js new file mode 100644 index 00000000..ea0c918a --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js @@ -0,0 +1,33 @@ +class MarketsComparatorInput extends HTMLElement { + + _assets = []; + _currencies = []; + _onChosenMarketSizeChange = (assetOrCurrency, marketSize) => { }; + + _assetOrCurrencyInput = undefined; + + connectedCallback() { + console.log("Markets comparator input!", this._assets, this._currencies); + this.innerHTML = ` + +
    + ${ this._assetOrCurrencyInput ?? 'Asset/Currency' } +
    +
      +
    +
    + `; + } + + _isAsset(assetOrCurrency) { + return assetOrCurrency ? this._assets.find(a => a.name == assetOrCurrency) : false; + } + + _isCurrency(assetOrCurrency) { + return assetOrCurrency ? this._currencies.find(c => c.name == assetOrCurrency) : false; + } +} + +export function register() { + customElements.define("markets-comparator-input-wc", MarketsComparatorInput); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/registry.js b/web-components-reuse/vue-app/src/components/lib/registry.js new file mode 100644 index 00000000..f08ffc14 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/registry.js @@ -0,0 +1,16 @@ +import * as AssetElement from './asset-element.js'; +import * as CurrencyElement from './currency-element.js'; +import * as TabsContainer from './tabs-container.js'; +import * as DropDown from './drop-down.js'; +import * as MarketsComparatorInput from './markets-comparator-input.js'; +import * as Customizations from './customizations.js'; + + +export function registerComponents() { + AssetElement.register(); + CurrencyElement.register(); + TabsContainer.register(); + DropDown.register(); + MarketsComparatorInput.register(); + Customizations.register(); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/tabs-container.js b/web-components-reuse/vue-app/src/components/lib/tabs-container.js new file mode 100644 index 00000000..8e977bbc --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/tabs-container.js @@ -0,0 +1,58 @@ +class TabsContainer extends HTMLElement { + + _activeTab = 0; + set activeTab(value) { + this._activeTab = value; + this._updateActiveTab(); + } + + activeTabClass = "underline"; + + + connectedCallback() { + this._render(); + } + + _render() { + this._tabsHeader = this.querySelector("[data-tabs-header]"); + this._tabsBody = this.querySelector("[data-tabs-body]"); + this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; + if (!this._tabsHeader) { + throw new Error("Tabs header must be defined and marked with data-tabs-header attribute!"); + } + if (!this._tabsBody) { + throw new Error("Tabs body must be defined and marked with data-tabs-body attribute!"); + } + + [...this._tabsHeader.children].forEach((tab, i) => { + tab.addEventListener('click', () => this.activeTab = i); + }); + + [...this._tabsBody.children].forEach((tab) => { + tab.classList.add("hidden"); + }); + + this._updateActiveTab(); + } + + _updateActiveTab() { + [...this._tabsHeader.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.add(this.activeTabClass); + } else { + tab.classList.remove(this.activeTabClass); + } + }); + [...this._tabsBody.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.remove('hidden'); + } else { + tab.classList.add('hidden'); + } + }); + } +} + +export function register() { + customElements.define('tabs-container', TabsContainer); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/utils.js b/web-components-reuse/vue-app/src/components/lib/utils.js new file mode 100644 index 00000000..5719e9e3 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/utils.js @@ -0,0 +1,16 @@ +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}`; +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/data/api.ts b/web-components-reuse/vue-app/src/data/api.ts new file mode 100644 index 00000000..338a905f --- /dev/null +++ b/web-components-reuse/vue-app/src/data/api.ts @@ -0,0 +1,35 @@ +import type { CurrencyCode } from "./currency-code"; +import { MockedApi } from "./mocked-api"; + +export interface Asset { + id: string; + name: string; + marketSize: number; + denomination: CurrencyCode +} + +export interface Currency { + code: CurrencyCode; + marketSize: number; + denomination: CurrencyCode; +} + +export interface ExchangeRate { + from: CurrencyCode; + to: CurrencyCode; + value: number; +} + +// TODO: error type +export interface Api { + + topAssets(denomination: CurrencyCode): Promise + + topCurrencies(denomination: CurrencyCode): Promise + + exchangeRates(): Promise + + exchangeRate(to: CurrencyCode): Promise +} + +export const api: Api = new MockedApi(); \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/data/currency-code.ts b/web-components-reuse/vue-app/src/data/currency-code.ts new file mode 100644 index 00000000..0c950591 --- /dev/null +++ b/web-components-reuse/vue-app/src/data/currency-code.ts @@ -0,0 +1,36 @@ +export interface CurrencyCode { + id: string; + name: string; +} + +export const USD = { + id: "USD", + name: "US Dollar" +}; + +export const EUR = { + id: "EUR", + name: "Euro" +}; + +export const JPY = { + id: "JPY", + name: "Japanese Yen" +}; + +export const GBP = { + id: "GBP", + name: "British Pound" +}; + +export const CNY = { + id: "CNY", + name: "Chinese Yuan" +}; + +export const PLN = { + id: "PLN", + name: "Polish Zloty" +} + +export const currencyCodes: CurrencyCode[] = [USD, EUR, JPY, GBP, CNY, PLN]; \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/data/mocked-api.ts b/web-components-reuse/vue-app/src/data/mocked-api.ts new file mode 100644 index 00000000..397a92b4 --- /dev/null +++ b/web-components-reuse/vue-app/src/data/mocked-api.ts @@ -0,0 +1,197 @@ +import type { Api, Asset, Currency, ExchangeRate } from "./api"; +import { type CurrencyCode, USD, EUR, JPY, GBP, CNY, PLN } from "./currency-code"; + +// TODO: common API not to duplicate it +export class MockedApi implements Api { + + // TODO: percentage of global market assets field + private assets = [ + { + id: "bonds", + name: "Bonds", + marketSize: 145.1e12, + denomination: USD + + }, + { + id: "stocks", + name: "Stocks", + marketSize: 126.7e12, + denomination: USD + + }, + { + id: "gold", + name: "Gold", + marketSize: 22.6e12, + denomination: USD + }, + { + id: "cash", + name: "Cash Reserves", + marketSize: 12.6e12, + denomination: USD + }, + { + id: "rlest", + name: "Real Estate", + marketSize: 12.5e12, + denomination: USD + }, + { + id: "btc", + name: "Bitcoin", + marketSize: 2.3e12, + denomination: USD + } + ]; + + private currencies = [ + { + code: USD, + marketSize: 6.639e12, + denomination: USD + }, + { + code: EUR, + marketSize: 2.292e12, + denomination: USD + }, + { + code: JPY, + marketSize: 1.253e12, + denomination: USD + }, + { + code: GBP, + marketSize: 968e9, + denomination: USD + }, + { + code: CNY, + marketSize: 526.2e9, + denomination: USD + }, + { + code: PLN, + marketSize: 13e9, + denomination: USD + } + ]; + + private baseUsdExchangeRates = [ + { + currencyCode: USD, + value: 1 + }, + { + currencyCode: EUR, + value: 0.85 + }, + { + currencyCode: JPY, + value: 148 + }, + { + currencyCode: GBP, + value: 0.73 + }, + { + currencyCode: CNY, + value: 7.11 + }, + { + currencyCode: PLN, + value: 3.63 + }, + ]; + private lastUsdExchangeRates = this.baseUsdExchangeRates; + + private nextExchangeRatesChange = false; + private nextAssetsValueChange = false; + private nextCurrenciesValueChange = false; + + setNextExchangeRatsChange(change: boolean) { + this.nextExchangeRatesChange = change; + } + + setNextAssetsValueChange(change: boolean) { + this.nextAssetsValueChange = change; + } + + setNextCurrenciesValueChange(change: boolean) { + this.nextCurrenciesValueChange = change; + } + + // TODO: order might change as well! + topAssets(denomination: CurrencyCode): Promise { + const exchangeRateValue = this.exchangeRateFor(denomination); + const denominatedAssets = this.assets.map(a => { + const nextValueMultiplier = this.nextAssetsValueChange && Math.random() > 0.5 ? 0.9 + (Math.random() * 0.2) : 1; + return { + ...a, marketSize: Math.round(a.marketSize * nextValueMultiplier * exchangeRateValue), + denomination + } + }); + + this.nextAssetsValueChange = false; + + return Promise.resolve(denominatedAssets); + } + + private exchangeRateFor(denomination: CurrencyCode): number { + const exchangeRate = this.lastUsdExchangeRates.find(er => er.currencyCode.id === denomination.id); + if (!exchangeRate) { + throw new Error(`There is no exchange rate for ${denomination} denomination!`); + } + return (exchangeRate as any).value; + } + + topCurrencies(denomination: CurrencyCode): Promise { + const exchangeRateValue = this.exchangeRateFor(denomination); + + const denominatedCurrencies = this.currencies.map(c => { + const nextValueMultiplier = this.nextCurrenciesValueChange && Math.random() > 0.5 ? 0.9 + (Math.random() * 0.2) : 1; + return { + ...c, marketSize: Math.round(c.marketSize * nextValueMultiplier * exchangeRateValue), + denomination + } + }); + + this.nextCurrenciesValueChange = false; + + return Promise.resolve(denominatedCurrencies); + } + + exchangeRates(): Promise { + const nextUsdExchangeRates = this.lastUsdExchangeRates.map(er => { + let nextValueMultiplier; + if (er.currencyCode == USD) { + nextValueMultiplier = 1; + } else { + nextValueMultiplier = this.nextExchangeRatesChange ? 0.95 + (Math.random() * 0.1) : 1; + } + return { ...er, value: er.value * nextValueMultiplier }; + }); + this.lastUsdExchangeRates = nextUsdExchangeRates; + + this.nextExchangeRatesChange = false; + + return Promise.resolve(nextUsdExchangeRates.map(er => ({ from: USD, to: er.currencyCode, value: er.value }))); + } + + async exchangeRate(to: CurrencyCode): Promise { + if (this.nextExchangeRatesChange) { + await this.exchangeRates(); + } + const exchangeRate = this.lastUsdExchangeRates.find(er => er.currencyCode.id == to.id); + if (!exchangeRate) { + throw new Error(`Couldn't find from USD to ${to} exchange rate`); + } + return Promise.resolve({ + from: USD, + value: exchangeRate.value, + to: exchangeRate.currencyCode + }); + } +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/data/updater.ts b/web-components-reuse/vue-app/src/data/updater.ts new file mode 100644 index 00000000..34f15477 --- /dev/null +++ b/web-components-reuse/vue-app/src/data/updater.ts @@ -0,0 +1,60 @@ +import { api, type Api } from "./api"; +import type { MockedApi } from "./mocked-api"; + +export class Updater { + + private exchangeRatesChangedListener = () => { }; + private assetsValueChangedListener = () => { }; + private currenciesValueChangedListener = () => { }; + private paused = false; + + constructor(private readonly api: Api) { + } + + public start() { + setInterval(() => this.update(), 1000); + } + + private async update() { + if (this.paused) { + return; + } + if (Math.random() > 0.75) { + (this.api as MockedApi).setNextExchangeRatsChange(true); + this.exchangeRatesChangedListener(); + } + if (Math.random() > 0.75) { + (this.api as MockedApi).setNextAssetsValueChange(true); + this.assetsValueChangedListener(); + } + if (Math.random() > 0.5) { + (this.api as MockedApi).setNextCurrenciesValueChange(true); + this.currenciesValueChangedListener(); + } + } + + public setExchangeRatesChangedListener(listener: () => void) { + this.exchangeRatesChangedListener = listener; + } + + public setAssetsValueChangedListener(listener: () => void) { + this.assetsValueChangedListener = listener; + } + + public setCurrenciesValueChangedListener(listener: () => void) { + this.currenciesValueChangedListener = listener; + } + + public setPaused(paused: boolean) { + this.paused = paused; + } +} + +let _updater: Updater | undefined; +export function useUpdater(): Updater { + if (!_updater) { + _updater = new Updater(api); + _updater.start(); + } + return _updater; +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/main.ts b/web-components-reuse/vue-app/src/main.ts new file mode 100644 index 00000000..1f92aa4d --- /dev/null +++ b/web-components-reuse/vue-app/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from 'vue'; +import './style.css'; +import App from './App.vue'; +import { createRouter, createWebHistory } from 'vue-router'; +import Home from './components/Home.vue'; + +// @ts-ignore +import { registerComponents } from './components/lib/registry.js'; + +registerComponents(); + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: '/', component: Home }, + ] +}); + +createApp(App) + .use(router) + .mount('#app'); \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/style.css b/web-components-reuse/vue-app/src/style.css new file mode 100644 index 00000000..a461c505 --- /dev/null +++ b/web-components-reuse/vue-app/src/style.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/vite-env.d.ts b/web-components-reuse/vue-app/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/web-components-reuse/vue-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web-components-reuse/vue-app/tsconfig.app.json b/web-components-reuse/vue-app/tsconfig.app.json new file mode 100644 index 00000000..f6b63e6f --- /dev/null +++ b/web-components-reuse/vue-app/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + /* Was true */ + "erasableSyntaxOnly": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/web-components-reuse/vue-app/tsconfig.json b/web-components-reuse/vue-app/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/web-components-reuse/vue-app/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web-components-reuse/vue-app/tsconfig.node.json b/web-components-reuse/vue-app/tsconfig.node.json new file mode 100644 index 00000000..9361bcc4 --- /dev/null +++ b/web-components-reuse/vue-app/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web-components-reuse/vue-app/vite.config.ts b/web-components-reuse/vue-app/vite.config.ts new file mode 100644 index 00000000..ed3f72fc --- /dev/null +++ b/web-components-reuse/vue-app/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue({ + template: { + compilerOptions: { + isCustomElement: (tag) => tag.includes('-') + } + } + }), tailwindcss()], +}) From d19eefd509f18d330bf3accac943e0918985e8b1 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 27 Sep 2025 11:13:55 +0200 Subject: [PATCH 02/13] WebComponents: vue - part 2 --- .../src/components/MarketsComparator.vue | 6 ++- .../vue-app/src/components/lib/drop-down.js | 38 ---------------- .../lib/markets-comparator-input.js | 43 ++++++++++++++++++- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/web-components-reuse/vue-app/src/components/MarketsComparator.vue b/web-components-reuse/vue-app/src/components/MarketsComparator.vue index c55c73c2..bcc028e6 100644 --- a/web-components-reuse/vue-app/src/components/MarketsComparator.vue +++ b/web-components-reuse/vue-app/src/components/MarketsComparator.vue @@ -42,12 +42,14 @@ const onChosenToMarketSizeChangeHandler = (assetOrCurrency: string, marketSize: \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/drop-down.js b/web-components-reuse/vue-app/src/components/lib/drop-down.js index 5d875ac2..05b91ee5 100644 --- a/web-components-reuse/vue-app/src/components/lib/drop-down.js +++ b/web-components-reuse/vue-app/src/components/lib/drop-down.js @@ -26,44 +26,6 @@ class DropDownContainer extends HTMLElement { } } -class DropDown extends HTMLElement { - - connectedCallback() { - const title = this.getAttribute("title"); - - // 3 guys: - // - //
    Title
    - // - //
  • - //
  • - //
    - //
    - // - - this.innerHTML = ` -
    -
    ${title}
    - -
    - `; - - const options = this.querySelector("ul"); - this._optionsElement = options; - - const container = this.querySelector("div"); - container.onclick = (e) => { - // Do not hide other, opened DropDowns - e.stopPropagation(); - options.classList.toggle("hidden"); - }; - } -} - export function register() { customElements.define("drop-down-container", DropDownContainer); - customElements.define("drop-down", DropDown); } \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js index ea0c918a..8601471f 100644 --- a/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js +++ b/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js @@ -2,23 +2,55 @@ class MarketsComparatorInput extends HTMLElement { _assets = []; _currencies = []; + _assetOrCurrencyOptions = []; _onChosenMarketSizeChange = (assetOrCurrency, marketSize) => { }; + set assets(value) { + this._assets = value; + this._recalculateAssetOrCurrencyOptions(); + this._rerenderOptionsHTML(); + } + + set currencies(value) { + this._currencies = value; + this._recalculateAssetOrCurrencyOptions(); + this._rerenderOptionsHTML(); + } + _assetOrCurrencyInput = undefined; connectedCallback() { - console.log("Markets comparator input!", this._assets, this._currencies); + console.log("Connected callback!"); + this._render(); + console.log("UL? ", this.querySelector("ul")); + } + + _render() { this.innerHTML = `
    - ${ this._assetOrCurrencyInput ?? 'Asset/Currency' } + ${this._assetOrCurrencyInput ?? 'Asset/Currency'}
      + ${this._optionsHTML()}
    `; } + _optionsHTML() { + return this._assetOrCurrencyOptions.map(o => + `
  • ${o.name}
  • `) + .join('\n'); + } + + _rerenderOptionsHTML() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._optionsHTML(); + } + } + _isAsset(assetOrCurrency) { return assetOrCurrency ? this._assets.find(a => a.name == assetOrCurrency) : false; } @@ -26,6 +58,13 @@ class MarketsComparatorInput extends HTMLElement { _isCurrency(assetOrCurrency) { return assetOrCurrency ? this._currencies.find(c => c.name == assetOrCurrency) : false; } + + _recalculateAssetOrCurrencyOptions() { + const assetOrCurrencyOptions = []; + this._assets.forEach(a => assetOrCurrencyOptions.push(a)); + this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); + this._assetOrCurrencyOptions = assetOrCurrencyOptions; + } } export function register() { From 619ba923c490f4454af7df382ab5f846525ade7e Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 27 Sep 2025 12:18:41 +0200 Subject: [PATCH 03/13] WebComponents: vue - part 2 --- web-components-reuse/.editorconfig | 3 - web-components-reuse/README.md | 5 + web-components-reuse/components/README.md | 1 + .../components/src/asset-element.js | 64 ++++ .../components/src/assets-and-currencies.js | 115 +++++++ .../components/src/currency-element.js | 63 ++++ .../components/src/drop-down.js | 31 ++ .../components/src/markets-comparator.js | 289 +++++++++++++++++ .../components/src/markets-header.js | 74 +++++ .../components/src/projections-calculator.js | 288 +++++++++++++++++ .../components/src/registry.js | 20 ++ .../components/src/tabs-container.js | 69 +++++ web-components-reuse/components/src/utils.js | 19 ++ web-components-reuse/vue-app/index.html | 1 - web-components-reuse/vue-app/public/vite.svg | 1 - .../vue-app/src/assets/vue.svg | 1 - .../vue-app/src/components/Home.vue | 140 +++++---- .../src/components/MarketsComparator.vue | 55 ---- .../src/components/lib/asset-element.js | 26 +- .../components/lib/assets-and-currencies.js | 115 +++++++ .../src/components/lib/currency-element.js | 22 +- .../src/components/lib/customizations.js | 13 - .../vue-app/src/components/lib/drop-down.js | 4 +- .../lib/markets-comparator-input.js | 72 ----- .../src/components/lib/markets-comparator.js | 286 +++++++++++++++++ .../src/components/lib/markets-header.js | 74 +++++ .../components/lib/projections-calculator.js | 291 ++++++++++++++++++ .../vue-app/src/components/lib/registry.js | 12 +- .../src/components/lib/tabs-container.js | 11 + .../vue-app/src/components/lib/utils.js | 5 +- .../src/components/old/MarketsComparator.vue | 83 +++++ .../{ => old}/MarketsComparatorInput.vue | 2 +- .../{ => old}/ProjectionsCalculator.vue | 4 +- .../{ => old}/ProjectionsResult.vue | 1 - .../vue-app/src/data/currency-code.ts | 18 +- .../vue-app/src/data/updater.ts | 2 +- 36 files changed, 2043 insertions(+), 237 deletions(-) delete mode 100644 web-components-reuse/.editorconfig create mode 100644 web-components-reuse/README.md create mode 100644 web-components-reuse/components/README.md create mode 100644 web-components-reuse/components/src/asset-element.js create mode 100644 web-components-reuse/components/src/assets-and-currencies.js create mode 100644 web-components-reuse/components/src/currency-element.js create mode 100644 web-components-reuse/components/src/drop-down.js create mode 100644 web-components-reuse/components/src/markets-comparator.js create mode 100644 web-components-reuse/components/src/markets-header.js create mode 100644 web-components-reuse/components/src/projections-calculator.js create mode 100644 web-components-reuse/components/src/registry.js create mode 100644 web-components-reuse/components/src/tabs-container.js create mode 100644 web-components-reuse/components/src/utils.js delete mode 100644 web-components-reuse/vue-app/public/vite.svg delete mode 100644 web-components-reuse/vue-app/src/assets/vue.svg delete mode 100644 web-components-reuse/vue-app/src/components/MarketsComparator.vue create mode 100644 web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/customizations.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js create mode 100644 web-components-reuse/vue-app/src/components/lib/markets-comparator.js create mode 100644 web-components-reuse/vue-app/src/components/lib/markets-header.js create mode 100644 web-components-reuse/vue-app/src/components/lib/projections-calculator.js create mode 100644 web-components-reuse/vue-app/src/components/old/MarketsComparator.vue rename web-components-reuse/vue-app/src/components/{ => old}/MarketsComparatorInput.vue (98%) rename web-components-reuse/vue-app/src/components/{ => old}/ProjectionsCalculator.vue (98%) rename web-components-reuse/vue-app/src/components/{ => old}/ProjectionsResult.vue (98%) diff --git a/web-components-reuse/.editorconfig b/web-components-reuse/.editorconfig deleted file mode 100644 index cb7baab8..00000000 --- a/web-components-reuse/.editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -[*.{js,jsx,ts,tsx}] -indent_style = space -indent_size = 2 \ No newline at end of file diff --git a/web-components-reuse/README.md b/web-components-reuse/README.md new file mode 100644 index 00000000..ba0694ee --- /dev/null +++ b/web-components-reuse/README.md @@ -0,0 +1,5 @@ +* 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 \ No newline at end of file diff --git a/web-components-reuse/components/README.md b/web-components-reuse/components/README.md new file mode 100644 index 00000000..235a66db --- /dev/null +++ b/web-components-reuse/components/README.md @@ -0,0 +1 @@ +Define all components in this dir and provide a script to generate a single file with all of them to reuse in various contexts. \ No newline at end of file diff --git a/web-components-reuse/components/src/asset-element.js b/web-components-reuse/components/src/asset-element.js new file mode 100644 index 00000000..c0b25da7 --- /dev/null +++ b/web-components-reuse/components/src/asset-element.js @@ -0,0 +1,64 @@ +// TODO: better styling & translations +import * as Utils from './utils.js'; + +class AssetElement extends HTMLElement { + + static observedAttributes = ["market-size", "denomination", "value-change-reason"]; + _previousMarketSize = null; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + this._render(); + } + + _render() { + const [id, name, marketSize, previousMarketSize, denomination, valueChangeReason] = [this.getAttribute("id"), this.getAttribute("name"), + this.getAttribute("market-size"), this.getAttribute("previous-market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reason") + ]; + if (id == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +
    + Previous market size:${Utils.formatMoney(this._previousMarketSize, denomination)} +
    +

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
    +

    ${name}

    +
    + Market size:${Utils.formatMoney(marketSize, denomination)} +
    + ${previousMarketSizeComponent} +
    + `; + } +} + +export function register() { + customElements.define("asset-element", AssetElement); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/assets-and-currencies.js b/web-components-reuse/components/src/assets-and-currencies.js new file mode 100644 index 00000000..4cb84999 --- /dev/null +++ b/web-components-reuse/components/src/assets-and-currencies.js @@ -0,0 +1,115 @@ +class AssetsAndCurrencies extends HTMLElement { + + _assets = []; + _assetsValueChangeReason = undefined; + _currencies = []; + _denomination = "USD"; + _assetsContainer = undefined; + _currenciesContainer = undefined; + + set assets(value) { + this._assets = value; + this._renderAssets(); + } + + set assetsValueChangeReason(value) { + this._assetsValueChangeReason = value; + this._renderAssets(); + } + + set currencies(value) { + this._currencies = value; + this._renderCurrencies(); + } + + set denomination(value) { + this._denomination = value; + this._renderAssets(); + this._renderCurrencies(); + } + + connectedCallback() { + this.innerHTML = ` + +
    + Assets + Currencies +
    +
    +
    + ${this._assetsHTML()} +
    +
    + ${this._currenciesHTML()} +
    +
    +
    `; + + 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 previousCurrencyMarketSize = previousAsset.getAttribute("market-size"); + if (previousCurrencyMarketSize != a.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousAsset.getAttribute("previous-market-size"); + } + } + + return ` + `; + }).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 previousCurrencyMarketSize = previousCurrency.getAttribute("market-size"); + if (previousCurrencyMarketSize != c.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousCurrency.getAttribute("previous-market-size"); + } + } + + return ` + `}) + .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); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/currency-element.js b/web-components-reuse/components/src/currency-element.js new file mode 100644 index 00000000..362103f3 --- /dev/null +++ b/web-components-reuse/components/src/currency-element.js @@ -0,0 +1,63 @@ +// TODO: better styling & translations +import * as Utils from './utils.js'; + +class CurrencyElement extends HTMLElement { + + static observedAttributes = ["market-size", "denomination"]; + + 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 == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%

    `; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
    +

    ${name}

    +
    + Daily turnover:${Utils.formatMoney(marketSize, denomination)} +
    +
    + Yearly turnover:${Utils.formatMoney(`${365 * parseInt(marketSize)}`, denomination)} +
    + ${previousMarketSizeComponent} +
    + `; + } +} + +export function register() { + customElements.define("currency-element", CurrencyElement); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/drop-down.js b/web-components-reuse/components/src/drop-down.js new file mode 100644 index 00000000..653defac --- /dev/null +++ b/web-components-reuse/components/src/drop-down.js @@ -0,0 +1,31 @@ +class DropDownContainer extends HTMLElement { + + connectedCallback() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; + const anchor = this.querySelector('[data-drop-down-anchor]') ?? this; + anchor.style = "position: relative; display: inline-block"; + + const options = this.querySelector("[data-drop-down-options]"); + if (!options) { + throw new Error("Options must be defined and marked with data-drop-down-options attribute!"); + } + options.style = `position: absolute; z-index: ${optionsZIndex}`; + options.classList.add("hidden"); + + anchor.onclick = (e) => { + // Do not hide other, opened DropDowns + e.stopPropagation(); + options.classList.toggle("hidden"); + }; + + window.addEventListener("click", e => { + if (e.target != anchor && e.target.parentNode != anchor) { + options.classList.add("hidden"); + } + }); + } +} + +export function register() { + customElements.define("drop-down-container", DropDownContainer); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/markets-comparator.js b/web-components-reuse/components/src/markets-comparator.js new file mode 100644 index 00000000..1efab493 --- /dev/null +++ b/web-components-reuse/components/src/markets-comparator.js @@ -0,0 +1,289 @@ +// TODO: document types! +class MarketsComparator extends HTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + _fromInputId = crypto.randomUUID(); + _toInputId = crypto.randomUUID(); + + _marketsComparatorInputFrom = null; + _marketsComparatorInputTo = null; + + _fromMarketSize = null; + _toMarketSize = null; + + set assets(value) { + if (this._marketsComparatorInputFrom) { + this._setValuesUsingAttributes(this._marketsComparatorInputFrom, value, "asset"); + } + if (this._marketsComparatorInputTo) { + this._setValuesUsingAttributes(this._marketsComparatorInputTo, value, "asset"); + } + } + + set currencies(value) { + if (this._marketsComparatorInputFrom) { + this._setValuesUsingAttributes(this._marketsComparatorInputFrom, value, "currency"); + } + if (this._marketsComparatorInputTo) { + this._setValuesUsingAttributes(this._marketsComparatorInputTo, value, "currency"); + } + } + + _setValuesUsingAttributes(element, values, prefix) { + for (let i = 0; i < values.length; i++) { + element.setAttribute(`${prefix}-${i}-name`, values[i].name); + element.setAttribute(`${prefix}-${i}-market-size`, values[i].marketSize); + } + element.setAttribute(`${prefix}-items`, values.length); + } + + connectedCallback() { + this._render(); + + this._chosenMarketSizeChangedEventHandler = e => { + const { componentId, name, marketSize } = e.detail; + if (componentId == this._fromInputId) { + this._fromMarketSize = marketSize; + this._renderComparisonElementHTML(); + document.dispatchEvent(new CustomEvent("mc:from-market-size-changed", { + detail: { + componentId: this.id, + name, marketSize + } + })); + } else if (componentId == this._toInputId) { + this._toMarketSize = marketSize; + this._renderComparisonElementHTML(); + document.dispatchEvent(new CustomEvent("mc:to-market-size-changed", { + detail: { + componentId: this.id, + name, marketSize + } + })); + } + } + document.addEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + disconnectedCallback() { + document.removeEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + this._fromInputId = crypto.randomUUID(); + this._toInputId = crypto.randomUUID(); + this._comparisonElementId = crypto.randomUUID(); + + this.innerHTML = ` +
    + +
    to
    + +
    ${this._comparisonElementHTML()}
    +
    + `; + + this._marketsComparatorInputFrom = document.getElementById(this._fromInputId); + this._marketsComparatorInputTo = document.getElementById(this._toInputId); + } + + _comparisonElementHTML() { + if (!this._fromMarketSize || !this._toMarketSize) { + return '-'; + } + return `${this._fromMarketSize.toExponential(3)} / ${this._toMarketSize.toExponential(3)} = ${this._chosenMarketsComparedValue()}`; + } + + _chosenMarketsComparedValue() { + if (!this._fromMarketSize || !this._toMarketSize) { + return 0; + } + return Math.round(this._fromMarketSize * 1000 / this._toMarketSize) / 1000.0; + } + + _renderComparisonElementHTML() { + document.getElementById(this._comparisonElementId).innerHTML = this._comparisonElementHTML(); + } +} + +function assetsFromAttributes(element) { + const countAttribute = element.getAttribute("asset-items"); + if (!countAttribute) { + return []; + } + const assets = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`asset-${i}-name`); + const marketSize = element.getAttribute(`asset-${i}-market-size`); + if (name && marketSize) { + assets.push({ name, marketSize: parseInt(marketSize) }); + } + } + return assets; +} + +function currenciesFromAttributes(element) { + const countAttribute = element.getAttribute("currency-items"); + if (!countAttribute) { + return []; + } + const currencies = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`currency-${i}-name`); + const marketSize = element.getAttribute(`currency-${i}-market-size`); + if (name && marketSize) { + currencies.push({ name, marketSize: parseInt(marketSize) }); + } + } + return currencies; +} + +class MarketsComparatorInput extends HTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + _assets = []; + _currencies = []; + _assetOrCurrencyOptions = []; + + set assets(value) { + this._assets = value; + this._recalculateAssetOrCurrencyOptions(); + } + + set currencies(value) { + this._currencies = value; + this._recalculateAssetOrCurrencyOptions(); + } + + _assetOrCurrencyInput = null; + _curencyTurnoverInputMultiplier = 1; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; + this.innerHTML = ` + +
    + ${this._dropDownHeaderHTML()} +
    +
      + ${this._optionsHTML()} +
    +
    + `; + + this._setOptionsClickHandlers(); + + this.querySelector('[data-drop-down-header]') + .querySelector("input")?.addEventListener("input", e => { + this._curencyTurnoverInputMultiplier = e.target.value; + this._calculateChosenMarketSizeChange(); + }); + } + + _optionsHTML() { + return this._assetOrCurrencyOptions.map(o => + `
  • ${o.name}
  • `) + .join('\n'); + } + + _dropDownHeaderHTML() { + let marketSizeHTML; + if (this._isAsset()) { + marketSizeHTML = `market size + + days turnover + `; + } else { + marketSizeHTML = ``; + } + return ` + ${this._assetOrCurrencyInput ?? 'Asset/Currency'} + ${marketSizeHTML} + `; + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._assetOrCurrencyInput = o.getAttribute("data-option-id"); + this._calculateChosenMarketSizeChange(); + this._render(); + }; + }); + } + + _renderOptionsHTML() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._optionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _isAsset() { + return this._assetOrCurrencyInput ? this._assets.find(a => a.name == this._assetOrCurrencyInput) : false; + } + + _isCurrency() { + return this._assetOrCurrencyInput ? this._currencies.find(c => c.name == this._assetOrCurrencyInput) : false; + } + + _recalculateAssetOrCurrencyOptions() { + const assetOrCurrencyOptions = []; + this._assets.forEach(a => assetOrCurrencyOptions.push(a)); + this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); + this._assetOrCurrencyOptions = assetOrCurrencyOptions; + + this._renderOptionsHTML(); + this._calculateChosenMarketSizeChange(); + } + + _calculateChosenMarketSizeChange() { + if (!this._assetOrCurrencyInput) { + return; + } + + const assetInput = this._assets.find(a => a.name == this._assetOrCurrencyInput); + const currencyInput = this._currencies.find(c => c.name == this._assetOrCurrencyInput); + + const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; + + document.dispatchEvent(new CustomEvent("mci:chosen-market-size-changed", { + detail: { + componentId: this.id, + name: this._assetOrCurrencyInput, + marketSize: inputMarketSize + } + })); + } +} + +export function register() { + customElements.define("markets-comparator-input", MarketsComparatorInput); + customElements.define('markets-comparator', MarketsComparator); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/markets-header.js b/web-components-reuse/components/src/markets-header.js new file mode 100644 index 00000000..655c1049 --- /dev/null +++ b/web-components-reuse/components/src/markets-header.js @@ -0,0 +1,74 @@ +class MarketsHeader extends HTMLElement { + + _denomination = 'USD'; + _liveUpdatesEnabled = true; + _denominationExchangeRates = []; + _liveUpdatesEnabledElement = undefined; + _denominationElement = undefined; + + + set denomination(value) { + this._denomination = value; + if (this._denominationElement) { + this._denominationElement = this._denomination; + } + } + + set denominationExchangeRates(value) { + this._denominationExchangeRates = value; + this._renderDenominationOptions(); + } + + connectedCallback() { + this.innerHTML = ` +
    Live Updates: ${this._liveUpdatesElementText()} +
    + Markets in + + ${this._denomination} +
      + ${this._denominationOptionsHTML()} +
    +
    + `; + + this._liveUpdatesEnabledElement = this.querySelector("span"); + this._liveUpdatesEnabledElement.onclick = () => { + this._liveUpdatesEnabled = !this._liveUpdatesEnabled; + this._liveUpdatesEnabledElement.textContent = this._liveUpdatesElementText(); + document.dispatchEvent(new CustomEvent('mh:live-updates-toggled', { detail: this._liveUpdatesEnabled })); + }; + + this._denominationElement = this.querySelector("drop-down-container > span"); + } + + _denominationOptionsHTML() { + return this._denominationExchangeRates.map(der => `
  • ${der.name}: ${der.exchangeRate}
  • `).join('\n'); + } + + _renderDenominationOptions() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._denominationOptionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._denomination = o.getAttribute("data-option-id"); + this._denominationElement.textContent = this._denomination; + document.dispatchEvent(new CustomEvent('mh:denomination-changed', { detail: this._denomination })); + }; + }); + } + + _liveUpdatesElementText() { + return `${this._liveUpdatesEnabled ? "ON" : "OFF"}`; + } +} + +export function register() { + customElements.define("markets-header", MarketsHeader); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/projections-calculator.js b/web-components-reuse/components/src/projections-calculator.js new file mode 100644 index 00000000..9887b01e --- /dev/null +++ b/web-components-reuse/components/src/projections-calculator.js @@ -0,0 +1,288 @@ +/** +* @typedef {Object} AssetOrCurrency +* @property {string} name +* @property {number} marketSize +* +* @typedef {Object} AssetOrCurrencyProjection +* @property {number} marketSize +* @property {number} growthRate +*/ + +class ProjectionsCalculator extends HTMLElement { + + /** @type {?AssetOrCurrency} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrency} */ + _assetOrCurrency2 = null; + /** @type {?number} */ + _assetOrCurrency1ExpectedGrowthRate = null; + /** @type {?number} */ + _assetOrCurrency2ExpectedGrowthRate = null; + /** @type {?number} */ + _customProjectionYears = null; + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._renderProjectionsResultsHTML(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._renderProjectionsResultsHTML(); + } + + _assetOrCurrency1Header = null; + _assetOrCurrency1Input = null; + _assetOrCurrency2Header = null; + _assetOrCurrency2Input = null; + _customProjectionInput = null; + _projectionsResultsContainer = null; + _customProjectionContainer = null; + _customProjectionTextElementId = crypto.randomUUID(); + _customProjectionResultContainerId = crypto.randomUUID(); + + connectedCallback() { + this.innerHTML = ` +

    Projections

    +
    +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    + +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    + +
    + ${this._projectionsResultsHTML()} +
    +
    + ${this._customProjectionHTML()} +
    +
    + `; + + const container = this.querySelector("div"); + + const divs = container.querySelectorAll("div"); + [this._assetOrCurrency1Header, this._assetOrCurrency2Header, this._projectionsResultsContainer] = divs; + this._customProjectionContainer = divs[divs.length - 1]; + + [this._assetOrCurrency1Input, this._assetOrCurrency2Input, + this._customProjectionInput] = container.querySelectorAll("input"); + + this._assetOrCurrency1Input.addEventListener("input", e => { + this._assetOrCurrency1ExpectedGrowthRate = e.target.value; + this._renderProjectionsResultsHTML(); + }); + this._assetOrCurrency2Input.addEventListener("input", e => { + this._assetOrCurrency2ExpectedGrowthRate = e.target.value; + this._renderProjectionsResultsHTML(); + }); + this._customProjectionInput.addEventListener("input", e => { + // TODO: is it really needed? + this._customProjectionYears = parseInt(e.target.value); + this._updateCustomProjectionText(); + this._updateCustomProjectionResult(); + }); + } + + _assetOrCurrencyHeaderText(assetOrCurrency) { + return `${assetOrCurrency ? assetOrCurrency.name : "Asset/Currency"} expected annual growth rate:`; + } + + _renderProjectionsResultsHTML() { + if (this._projectionsResultsContainer && this._customProjectionContainer) { + this._projectionsResultsContainer.innerHTML = this._projectionsResultsHTML(); + this._updateCustomProjectionResult(); + } + } + + _projectionsResultsHTML() { + const inYearsText = (years) => `In ${years} ${years == 1 ? 'year' : 'years'}`; + const currentYear = new Date().getFullYear(); + return [1, 5, 10].map(y => ` +
    ${inYearsText(y)} (${currentYear + y}):
    + ${this._projectionsResultHTML(y)}`) + .join('\n'); + } + + _projectionsResultHTML(years) { + const ac1 = this._assetOrCurrency1WithExpectedGrowthRate(); + const ac2 = this._assetOrCurrency2WithExpectedGrowthRate(); + if (years != null && ac1 != null && ac2 != null) { + return ` + + `; + } + return '
    -
    '; + } + + _customProjectionHTML() { + return ` + In + ${this._customProjectionYearText()}: +
    ${this._projectionsResultHTML(this._customProjectionYears)}
    + `; + } + + _customProjectionYearText() { + const currentYear = new Date().getFullYear(); + return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? + `${currentYear} + N` : currentYear + this._customProjectionYears) + ')'; + } + + _updateCustomProjectionText() { + document.getElementById(this._customProjectionTextElementId).textContent = this._customProjectionYearText(); + } + + _updateCustomProjectionResult() { + document.getElementById(this._customProjectionResultContainerId).innerHTML = this._projectionsResultHTML(this._customProjectionYears); + } + + _assetOrCurrency1WithExpectedGrowthRate() { + if (this._assetOrCurrency1 && this._assetOrCurrency1ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency1.marketSize, growthRate: this._assetOrCurrency1ExpectedGrowthRate }; + } + return null; + } + + _assetOrCurrency2WithExpectedGrowthRate() { + if (this._assetOrCurrency2 && this._assetOrCurrency2ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency2.marketSize, growthRate: this._assetOrCurrency2ExpectedGrowthRate }; + } + return null; + } +} + +class ProjectionsResult extends HTMLElement { + + static observedAttributes = [ + "years", + "asset-or-currency-1-market-size", "asset-or-currency-1-growth-rate", + "asset-or-currency-2-market-size", "asset-or-currency-2-growth-rate" + ]; + + /** @type {number} */ + _years = 1; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency2 = null; + + set years(value) { + this._years = value; + this._render(); + } + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._render(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._render(); + } + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name.includes("asset-or-currency-1")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-1"); + if (assetOrCurrency) { + this.assetOrCurrency1 = assetOrCurrency; + } + } else if (name.includes("asset-or-currency-2")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-2"); + if (assetOrCurrency) { + this.assetOrCurrency2 = assetOrCurrency; + } + } else if (name == 'years') { + this.years = parseInt(newValue); + } + } + + _assetOrCurrencyFromAttributes(prefix) { + const marketSize = this.getAttribute(`${prefix}-market-size`); + const growthRate = this.getAttribute(`${prefix}-growth-rate`); + if (marketSize && growthRate) { + return { marketSize: parseInt(marketSize), growthRate: parseInt(growthRate) }; + } + return null; + } + + _render() { + const nominator = this._exponentialNumberString(this._projectionNumerator()); + const denominator = this._exponentialNumberString(this._projectionDenominator()); + this.innerHTML = ` +
    ${nominator} / ${denominator} = ${this._projection()} +
    + `; + } + + _exponentialNumberString(n) { + return n?.toExponential(3) ?? ''; + } + + _marketSizeChangedByRate(marketSize, growthRate, decrease = false) { + let changedMarketSize; + if (decrease) { + changedMarketSize = marketSize - (marketSize * growthRate / 100.0); + } else { + changedMarketSize = marketSize + (marketSize * growthRate / 100.0); + } + if (changedMarketSize <= 0 || Number.isNaN(changedMarketSize)) { + return null; + } + return changedMarketSize; + } + + _marketSizeChangedByRateInGivenYears(marketSize, growthRate, years) { + const negativeYears = years < 0; + let increasedMarketSize = marketSize; + for (let i = 0; i < Math.abs(years); i++) { + increasedMarketSize = this._marketSizeChangedByRate(increasedMarketSize, growthRate, negativeYears); + if (!increasedMarketSize) { + return null; + } + } + return increasedMarketSize; + } + + _projectionNumerator() { + if (this._assetOrCurrency1) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency1.marketSize, + this._assetOrCurrency1.growthRate, + this._years); + } + return null; + } + + _projectionDenominator() { + if (this._assetOrCurrency2) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency2.marketSize, + this._assetOrCurrency2.growthRate, + this._years); + } + return null; + } + + _projection() { + const numerator = this._projectionNumerator(); + const denominator = this._projectionDenominator(); + if (numerator && denominator) { + return Math.round(numerator * 1000 / denominator) / 1000.0; + } + return ''; + } +} + +export function register() { + customElements.define("projections-result", ProjectionsResult); + customElements.define("projections-calculator", ProjectionsCalculator); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/registry.js b/web-components-reuse/components/src/registry.js new file mode 100644 index 00000000..5bebb6c6 --- /dev/null +++ b/web-components-reuse/components/src/registry.js @@ -0,0 +1,20 @@ +import * as AssetElement from './asset-element.js'; +import * as CurrencyElement from './currency-element.js'; +import * as TabsContainer from './tabs-container.js'; +import * as DropDown from './drop-down.js'; +import * as MarketsHeader from './markets-header.js'; +import * as AssetsAndCurrencies from './assets-and-currencies.js'; +import * as MarketsComparator from './markets-comparator.js'; +import * as ProjectionsCalculator from './projections-calculator.js'; + + +export function registerComponents() { + AssetElement.register(); + CurrencyElement.register(); + TabsContainer.register(); + DropDown.register(); + MarketsHeader.register(); + AssetsAndCurrencies.register(); + MarketsComparator.register(); + ProjectionsCalculator.register(); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/tabs-container.js b/web-components-reuse/components/src/tabs-container.js new file mode 100644 index 00000000..9f6e591e --- /dev/null +++ b/web-components-reuse/components/src/tabs-container.js @@ -0,0 +1,69 @@ +class TabsContainer extends HTMLElement { + + _activeTab = 0; + set activeTab(value) { + this._activeTab = value; + this._updateActiveTab(); + } + + activeTabClass = "underline"; + + + connectedCallback() { + this._render(); + } + + _render() { + this._tabsHeader = this.querySelector("[data-tabs-header]"); + this._tabsBody = this.querySelector("[data-tabs-body]"); + this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; + if (!this._tabsHeader) { + throw new Error("Tabs header must be defined and marked with data-tabs-header attribute!"); + } + if (!this._tabsBody) { + throw new Error("Tabs body must be defined and marked with data-tabs-body attribute!"); + } + + [...this._tabsHeader.children].forEach((tab, i) => { + tab.addEventListener('click', () => this.activeTab = i); + }); + + [...this._tabsBody.children].forEach((tab) => { + tab.classList.add("hidden"); + }); + + this._updateActiveTab(); + } + + _updateActiveTab() { + [...this._tabsHeader.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.add(this.activeTabClass); + } else { + tab.classList.remove(this.activeTabClass); + } + }); + [...this._tabsBody.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.remove('hidden'); + } else { + tab.classList.add('hidden'); + } + }); + } +} + +class TabHeader extends HTMLElement { + + connectedCallback() { + this.classList.add("text-2xl"); + this.classList.add("p-2"); + this.classList.add("cursor-pointer"); + this.classList.add("grow"); + } +} + +export function register() { + customElements.define('tabs-container', TabsContainer); + customElements.define('tab-header', TabHeader); +} \ No newline at end of file diff --git a/web-components-reuse/components/src/utils.js b/web-components-reuse/components/src/utils.js new file mode 100644 index 00000000..d239aca5 --- /dev/null +++ b/web-components-reuse/components/src/utils.js @@ -0,0 +1,19 @@ +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}`; +} + +// TODO +export const translations = {}; \ No newline at end of file diff --git a/web-components-reuse/vue-app/index.html b/web-components-reuse/vue-app/index.html index 0be5b153..6ef31d4f 100644 --- a/web-components-reuse/vue-app/index.html +++ b/web-components-reuse/vue-app/index.html @@ -2,7 +2,6 @@ - Markets diff --git a/web-components-reuse/vue-app/public/vite.svg b/web-components-reuse/vue-app/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web-components-reuse/vue-app/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/assets/vue.svg b/web-components-reuse/vue-app/src/assets/vue.svg deleted file mode 100644 index 770e9d33..00000000 --- a/web-components-reuse/vue-app/src/assets/vue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/Home.vue b/web-components-reuse/vue-app/src/components/Home.vue index f11b9a28..f832801d 100644 --- a/web-components-reuse/vue-app/src/components/Home.vue +++ b/web-components-reuse/vue-app/src/components/Home.vue @@ -1,22 +1,22 @@ - - diff --git a/web-components-reuse/vue-app/src/components/MarketsComparator.vue b/web-components-reuse/vue-app/src/components/MarketsComparator.vue deleted file mode 100644 index bcc028e6..00000000 --- a/web-components-reuse/vue-app/src/components/MarketsComparator.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/asset-element.js b/web-components-reuse/vue-app/src/components/lib/asset-element.js index c8ca3687..46875284 100644 --- a/web-components-reuse/vue-app/src/components/lib/asset-element.js +++ b/web-components-reuse/vue-app/src/components/lib/asset-element.js @@ -3,29 +3,37 @@ import * as Utils from './utils.js'; class AssetElement extends HTMLElement { - static observedAttributes = ["market-size", "denomination", "value-change-reasons"]; - _previousMarketSize = null; - + /** + * Supported attributes + * {string} id: asset id + * {string} name: asset name + * {number} market-size + * {number} previous-market-size + * {string} value-change-reason: optional reason of the market size change + * {string} denomination + * {string} class: additional class to append to the root div + */ connectedCallback() { this._render(); } attributeChangedCallback(name, oldValue, newValue) { this._render(); - if (name == 'market-size') { - this._previousMarketSize = newValue; - } } _render() { - const [id, name, marketSize, denomination, valueChangeReasons] = [this.getAttribute("id"), this.getAttribute("name"), - this.getAttribute("market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reasons") + const [id, name, marketSize, previousMarketSize, denomination, valueChangeReason] = [this.getAttribute("id"), this.getAttribute("name"), + this.getAttribute("market-size"), this.getAttribute("previous-market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reason") ]; if (id == undefined || name == undefined) { return; } const classesToAppend = this.getAttribute("class"); + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + let previousMarketSizeComponent; if (this._previousMarketSize && this._previousMarketSize != marketSize) { const previousMarketSizeInt = parseInt(this._previousMarketSize); @@ -41,7 +49,7 @@ class AssetElement extends HTMLElement {
    Previous market size:${Utils.formatMoney(this._previousMarketSize, denomination)}
    -

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%; ${valueChangeReasons ?? 'UNKNOWN'}

    `; +

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; } else { previousMarketSizeComponent = ``; } diff --git a/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js b/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js new file mode 100644 index 00000000..4cb84999 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js @@ -0,0 +1,115 @@ +class AssetsAndCurrencies extends HTMLElement { + + _assets = []; + _assetsValueChangeReason = undefined; + _currencies = []; + _denomination = "USD"; + _assetsContainer = undefined; + _currenciesContainer = undefined; + + set assets(value) { + this._assets = value; + this._renderAssets(); + } + + set assetsValueChangeReason(value) { + this._assetsValueChangeReason = value; + this._renderAssets(); + } + + set currencies(value) { + this._currencies = value; + this._renderCurrencies(); + } + + set denomination(value) { + this._denomination = value; + this._renderAssets(); + this._renderCurrencies(); + } + + connectedCallback() { + this.innerHTML = ` + +
    + Assets + Currencies +
    +
    +
    + ${this._assetsHTML()} +
    +
    + ${this._currenciesHTML()} +
    +
    +
    `; + + 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 previousCurrencyMarketSize = previousAsset.getAttribute("market-size"); + if (previousCurrencyMarketSize != a.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousAsset.getAttribute("previous-market-size"); + } + } + + return ` + `; + }).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 previousCurrencyMarketSize = previousCurrency.getAttribute("market-size"); + if (previousCurrencyMarketSize != c.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousCurrency.getAttribute("previous-market-size"); + } + } + + return ` + `}) + .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); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/currency-element.js b/web-components-reuse/vue-app/src/components/lib/currency-element.js index bafc5524..79a72281 100644 --- a/web-components-reuse/vue-app/src/components/lib/currency-element.js +++ b/web-components-reuse/vue-app/src/components/lib/currency-element.js @@ -3,28 +3,36 @@ import * as Utils from './utils.js'; class CurrencyElement extends HTMLElement { - static observedAttributes = ["market-size", "denomination"]; - + /** + * Supported attributes + * {string} id: currency id + * {string} name: currency 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(); - if (name == 'market-size') { - this._previousMarketSize = newValue; - } } _render() { - const [id, name, marketSize, denomination] = [this.getAttribute("id"), this.getAttribute("name"), - this.getAttribute("market-size"), this.getAttribute("denomination") + 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 == undefined || name == undefined) { return; } const classesToAppend = this.getAttribute("class"); + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + let previousMarketSizeComponent; if (this._previousMarketSize && this._previousMarketSize != marketSize) { const previousMarketSizeInt = parseInt(this._previousMarketSize); diff --git a/web-components-reuse/vue-app/src/components/lib/customizations.js b/web-components-reuse/vue-app/src/components/lib/customizations.js deleted file mode 100644 index 94d1367d..00000000 --- a/web-components-reuse/vue-app/src/components/lib/customizations.js +++ /dev/null @@ -1,13 +0,0 @@ -class TabHeader extends HTMLElement { - - connectedCallback() { - this.classList.add("text-2xl"); - this.classList.add("p-2"); - this.classList.add("cursor-pointer"); - this.classList.add("grow"); - } -} - -export function register() { - customElements.define('tab-header', TabHeader); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/drop-down.js b/web-components-reuse/vue-app/src/components/lib/drop-down.js index 05b91ee5..653defac 100644 --- a/web-components-reuse/vue-app/src/components/lib/drop-down.js +++ b/web-components-reuse/vue-app/src/components/lib/drop-down.js @@ -1,6 +1,7 @@ class DropDownContainer extends HTMLElement { connectedCallback() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; const anchor = this.querySelector('[data-drop-down-anchor]') ?? this; anchor.style = "position: relative; display: inline-block"; @@ -8,7 +9,7 @@ class DropDownContainer extends HTMLElement { if (!options) { throw new Error("Options must be defined and marked with data-drop-down-options attribute!"); } - options.style = "position: absolute; z-index: 99"; + options.style = `position: absolute; z-index: ${optionsZIndex}`; options.classList.add("hidden"); anchor.onclick = (e) => { @@ -19,7 +20,6 @@ class DropDownContainer extends HTMLElement { window.addEventListener("click", e => { if (e.target != anchor && e.target.parentNode != anchor) { - console.log("Global DropDown close!"); options.classList.add("hidden"); } }); diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js deleted file mode 100644 index 8601471f..00000000 --- a/web-components-reuse/vue-app/src/components/lib/markets-comparator-input.js +++ /dev/null @@ -1,72 +0,0 @@ -class MarketsComparatorInput extends HTMLElement { - - _assets = []; - _currencies = []; - _assetOrCurrencyOptions = []; - _onChosenMarketSizeChange = (assetOrCurrency, marketSize) => { }; - - set assets(value) { - this._assets = value; - this._recalculateAssetOrCurrencyOptions(); - this._rerenderOptionsHTML(); - } - - set currencies(value) { - this._currencies = value; - this._recalculateAssetOrCurrencyOptions(); - this._rerenderOptionsHTML(); - } - - _assetOrCurrencyInput = undefined; - - connectedCallback() { - console.log("Connected callback!"); - this._render(); - console.log("UL? ", this.querySelector("ul")); - } - - _render() { - this.innerHTML = ` - -
    - ${this._assetOrCurrencyInput ?? 'Asset/Currency'} -
    -
      - ${this._optionsHTML()} -
    -
    - `; - } - - _optionsHTML() { - return this._assetOrCurrencyOptions.map(o => - `
  • ${o.name}
  • `) - .join('\n'); - } - - _rerenderOptionsHTML() { - const optionsContainer = this.querySelector("ul"); - if (optionsContainer) { - optionsContainer.innerHTML = this._optionsHTML(); - } - } - - _isAsset(assetOrCurrency) { - return assetOrCurrency ? this._assets.find(a => a.name == assetOrCurrency) : false; - } - - _isCurrency(assetOrCurrency) { - return assetOrCurrency ? this._currencies.find(c => c.name == assetOrCurrency) : false; - } - - _recalculateAssetOrCurrencyOptions() { - const assetOrCurrencyOptions = []; - this._assets.forEach(a => assetOrCurrencyOptions.push(a)); - this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); - this._assetOrCurrencyOptions = assetOrCurrencyOptions; - } -} - -export function register() { - customElements.define("markets-comparator-input-wc", MarketsComparatorInput); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator.js new file mode 100644 index 00000000..148c5194 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/markets-comparator.js @@ -0,0 +1,286 @@ +/** +* @typedef {Object} AssetOrCurrency +* @property {string} name +* @property {number} marketSize +*/ + +class MarketsComparator extends HTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + _fromMarketsComparatorInput = null; + _toMarketsComparatorInput = null; + _comparisonElement = null; + + _fromMarketSize = null; + _toMarketSize = null; + + /** @type {AssetOrCurrency[]} */ + set assets(value) { + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "asset"); + } + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "asset"); + } + } + + /** @type {AssetOrCurrency[]} */ + set currencies(value) { + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "currency"); + } + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "currency"); + } + } + + _setValuesUsingAttributes(element, values, prefix) { + for (let i = 0; i < values.length; i++) { + element.setAttribute(`${prefix}-${i}-name`, values[i].name); + element.setAttribute(`${prefix}-${i}-market-size`, values[i].marketSize); + } + element.setAttribute(`${prefix}-items`, values.length); + } + + connectedCallback() { + this._render(); + + this._chosenMarketSizeChangedEventHandler = e => { + const { componentId, name, marketSize } = e.detail; + if (e.target === this._fromMarketsComparatorInput) { + this._fromMarketSize = marketSize; + this._renderComparisonElementHTML(); + this.dispatchEvent(new CustomEvent("mc:from-market-size-changed", { + bubbles: true, + detail: { name, marketSize } + })); + } else if (e.target === this._toMarketsComparatorInput) { + this._toMarketSize = marketSize; + this._renderComparisonElementHTML(); + this.dispatchEvent(new CustomEvent("mc:to-market-size-changed", { + bubbles: true, + detail: { name, marketSize } + })); + } + } + document.addEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + disconnectedCallback() { + document.removeEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + this.innerHTML = ` +
    + +
    to
    + +
    ${this._comparisonElementHTML()}
    +
    + `; + + [this._fromMarketsComparatorInput, this._toMarketsComparatorInput] = this.querySelectorAll("markets-comparator-input"); + this._comparisonElement = this.querySelector('[data-comparison-element]'); + } + + _comparisonElementHTML() { + if (!this._fromMarketSize || !this._toMarketSize) { + return '-'; + } + return `${this._fromMarketSize.toExponential(3)} / ${this._toMarketSize.toExponential(3)} = ${this._chosenMarketsComparedValue()}`; + } + + _chosenMarketsComparedValue() { + if (!this._fromMarketSize || !this._toMarketSize) { + return 0; + } + return Math.round(this._fromMarketSize * 1000 / this._toMarketSize) / 1000.0; + } + + _renderComparisonElementHTML() { + this._comparisonElement.innerHTML = this._comparisonElementHTML(); + } +} + +function assetsFromAttributes(element) { + const countAttribute = element.getAttribute("asset-items"); + if (!countAttribute) { + return []; + } + const assets = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`asset-${i}-name`); + const marketSize = element.getAttribute(`asset-${i}-market-size`); + if (name && marketSize) { + assets.push({ name, marketSize: parseInt(marketSize) }); + } + } + return assets; +} + +function currenciesFromAttributes(element) { + const countAttribute = element.getAttribute("currency-items"); + if (!countAttribute) { + return []; + } + const currencies = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`currency-${i}-name`); + const marketSize = element.getAttribute(`currency-${i}-market-size`); + if (name && marketSize) { + currencies.push({ name, marketSize: parseInt(marketSize) }); + } + } + return currencies; +} + +class MarketsComparatorInput extends HTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + /** @type {AssetOrCurrency[]} */ + _assets = []; + /** @type {AssetOrCurrency[]} */ + _currencies = []; + /** @type {AssetOrCurrency[]} */ + _assetOrCurrencyOptions = []; + + set assets(value) { + this._assets = value; + this._recalculateAssetOrCurrencyOptions(); + } + + set currencies(value) { + this._currencies = value; + this._recalculateAssetOrCurrencyOptions(); + } + + _assetOrCurrencyInput = null; + _curencyTurnoverInputMultiplier = 1; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; + this.innerHTML = ` + +
    + ${this._dropDownHeaderHTML()} +
    +
      + ${this._optionsHTML()} +
    +
    + `; + + this._setOptionsClickHandlers(); + + this.querySelector('[data-drop-down-header]') + .querySelector("input")?.addEventListener("input", e => { + this._curencyTurnoverInputMultiplier = e.target.value; + this._calculateChosenMarketSizeChange(); + }); + } + + _optionsHTML() { + return this._assetOrCurrencyOptions.map(o => + `
  • ${o.name}
  • `) + .join('\n'); + } + + _dropDownHeaderHTML() { + let marketSizeHTML; + if (this._isAsset()) { + marketSizeHTML = `market size + + days turnover + `; + } else { + marketSizeHTML = ``; + } + return ` + ${this._assetOrCurrencyInput ?? 'Asset/Currency'} + ${marketSizeHTML} + `; + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._assetOrCurrencyInput = o.getAttribute("data-option-id"); + this._calculateChosenMarketSizeChange(); + this._render(); + }; + }); + } + + _renderOptionsHTML() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._optionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _isAsset() { + return this._assetOrCurrencyInput ? this._assets.find(a => a.name == this._assetOrCurrencyInput) : false; + } + + _isCurrency() { + return this._assetOrCurrencyInput ? this._currencies.find(c => c.name == this._assetOrCurrencyInput) : false; + } + + _recalculateAssetOrCurrencyOptions() { + const assetOrCurrencyOptions = []; + this._assets.forEach(a => assetOrCurrencyOptions.push(a)); + this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); + this._assetOrCurrencyOptions = assetOrCurrencyOptions; + + this._renderOptionsHTML(); + this._calculateChosenMarketSizeChange(); + } + + _calculateChosenMarketSizeChange() { + if (!this._assetOrCurrencyInput) { + return; + } + + const assetInput = this._assets.find(a => a.name == this._assetOrCurrencyInput); + const currencyInput = this._currencies.find(c => c.name == this._assetOrCurrencyInput); + + const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; + + this.dispatchEvent(new CustomEvent("mci:chosen-market-size-changed", { + bubbles: true, + detail: { name: this._assetOrCurrencyInput, marketSize: inputMarketSize } + })); + } +} + +export function register() { + customElements.define("markets-comparator-input", MarketsComparatorInput); + customElements.define('markets-comparator', MarketsComparator); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-header.js b/web-components-reuse/vue-app/src/components/lib/markets-header.js new file mode 100644 index 00000000..cd9e43b1 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/markets-header.js @@ -0,0 +1,74 @@ +class MarketsHeader extends HTMLElement { + + _denomination = 'USD'; + _liveUpdatesEnabled = true; + _denominationExchangeRates = []; + _liveUpdatesEnabledElement = null; + _denominationElement = null; + + + set denomination(value) { + this._denomination = value; + if (this._denominationElement) { + this._denominationElement = this._denomination; + } + } + + set denominationExchangeRates(value) { + this._denominationExchangeRates = value; + this._renderDenominationOptions(); + } + + connectedCallback() { + this.innerHTML = ` +
    Live Updates: ${this._liveUpdatesElementText()} +
    + Markets in + + ${this._denomination} +
      + ${this._denominationOptionsHTML()} +
    +
    + `; + + this._liveUpdatesEnabledElement = this.querySelector("span"); + this._liveUpdatesEnabledElement.onclick = () => { + this._liveUpdatesEnabled = !this._liveUpdatesEnabled; + this._liveUpdatesEnabledElement.textContent = this._liveUpdatesElementText(); + document.dispatchEvent(new CustomEvent('mh:live-updates-toggled', { detail: this._liveUpdatesEnabled })); + }; + + this._denominationElement = this.querySelector("drop-down-container > span"); + } + + _denominationOptionsHTML() { + return this._denominationExchangeRates.map(der => `
  • ${der.name}: ${der.exchangeRate}
  • `).join('\n'); + } + + _renderDenominationOptions() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._denominationOptionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._denomination = o.getAttribute("data-option-id"); + this._denominationElement.textContent = this._denomination; + document.dispatchEvent(new CustomEvent('mh:denomination-changed', { detail: this._denomination })); + }; + }); + } + + _liveUpdatesElementText() { + return `${this._liveUpdatesEnabled ? "ON" : "OFF"}`; + } +} + +export function register() { + customElements.define("markets-header", MarketsHeader); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/projections-calculator.js b/web-components-reuse/vue-app/src/components/lib/projections-calculator.js new file mode 100644 index 00000000..c4fdcb57 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/projections-calculator.js @@ -0,0 +1,291 @@ +/** +* @typedef {Object} AssetOrCurrency +* @property {string} name +* @property {number} marketSize +* +* @typedef {Object} AssetOrCurrencyProjection +* @property {number} marketSize +* @property {number} growthRate +*/ + +class ProjectionsCalculator extends HTMLElement { + + /** @type {?AssetOrCurrency} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrency} */ + _assetOrCurrency2 = null; + /** @type {?number} */ + _assetOrCurrency1ExpectedGrowthRate = null; + /** @type {?number} */ + _assetOrCurrency2ExpectedGrowthRate = null; + /** @type {?number} */ + _customProjectionYears = null; + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._renderProjectionsResultsHTML(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._renderProjectionsResultsHTML(); + } + + _assetOrCurrency1Header = null; + _assetOrCurrency1Input = null; + _assetOrCurrency2Header = null; + _assetOrCurrency2Input = null; + _customProjectionInput = null; + _projectionsResultsContainer = null; + _customProjectionContainer = null; + _customProjectionTextElement = null; + _customProjectionResultContainer = null; + + connectedCallback() { + this.innerHTML = ` +

    Projections

    +
    +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    + +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    + +
    + ${this._projectionsResultsHTML()} +
    +
    + ${this._customProjectionHTML()} +
    +
    + `; + + const container = this.querySelector("div"); + + const divs = container.querySelectorAll("div"); + [this._assetOrCurrency1Header, this._assetOrCurrency2Header, this._projectionsResultsContainer] = divs; + this._customProjectionContainer = divs[divs.length - 1]; + + [this._assetOrCurrency1Input, this._assetOrCurrency2Input, + this._customProjectionInput] = container.querySelectorAll("input"); + + this._assetOrCurrency1Input.addEventListener("input", e => { + this._assetOrCurrency1ExpectedGrowthRate = e.target.value; + this._renderProjectionsResultsHTML(); + }); + this._assetOrCurrency2Input.addEventListener("input", e => { + this._assetOrCurrency2ExpectedGrowthRate = e.target.value; + this._renderProjectionsResultsHTML(); + }); + this._customProjectionInput.addEventListener("input", e => { + // TODO: is it really needed? + this._customProjectionYears = parseInt(e.target.value); + this._updateCustomProjectionText(); + this._updateCustomProjectionResult(); + }); + + this._customProjectionTextElement = this.querySelector('[data-custom-projection-text-element]'); + this._customProjectionResultContainer = this.querySelector('[data-custom-projection-result-container]'); + } + + _assetOrCurrencyHeaderText(assetOrCurrency) { + return `${assetOrCurrency ? assetOrCurrency.name : "Asset/Currency"} expected annual growth rate:`; + } + + _renderProjectionsResultsHTML() { + if (this._projectionsResultsContainer && this._customProjectionContainer) { + this._projectionsResultsContainer.innerHTML = this._projectionsResultsHTML(); + this._updateCustomProjectionResult(); + } + } + + _projectionsResultsHTML() { + const inYearsText = (years) => `In ${years} ${years == 1 ? 'year' : 'years'}`; + const currentYear = new Date().getFullYear(); + return [1, 5, 10].map(y => ` +
    ${inYearsText(y)} (${currentYear + y}):
    + ${this._projectionsResultHTML(y)}`) + .join('\n'); + } + + _projectionsResultHTML(years) { + const ac1 = this._assetOrCurrency1WithExpectedGrowthRate(); + const ac2 = this._assetOrCurrency2WithExpectedGrowthRate(); + if (years != null && ac1 != null && ac2 != null) { + return ` + + `; + } + return '
    -
    '; + } + + _customProjectionHTML() { + return ` + In + ${this._customProjectionYearText()}: +
    ${this._projectionsResultHTML(this._customProjectionYears)}
    + `; + } + + _customProjectionYearText() { + const currentYear = new Date().getFullYear(); + return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? + `${currentYear} + N` : currentYear + this._customProjectionYears) + ')'; + } + + _updateCustomProjectionText() { + this._customProjectionTextElement.textContent = this._customProjectionYearText(); + } + + _updateCustomProjectionResult() { + this._customProjectionContainer.innerHTML = this._projectionsResultHTML(this._customProjectionYears); + } + + _assetOrCurrency1WithExpectedGrowthRate() { + if (this._assetOrCurrency1 && this._assetOrCurrency1ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency1.marketSize, growthRate: this._assetOrCurrency1ExpectedGrowthRate }; + } + return null; + } + + _assetOrCurrency2WithExpectedGrowthRate() { + if (this._assetOrCurrency2 && this._assetOrCurrency2ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency2.marketSize, growthRate: this._assetOrCurrency2ExpectedGrowthRate }; + } + return null; + } +} + +class ProjectionsResult extends HTMLElement { + + static observedAttributes = [ + "years", + "asset-or-currency-1-market-size", "asset-or-currency-1-growth-rate", + "asset-or-currency-2-market-size", "asset-or-currency-2-growth-rate" + ]; + + /** @type {number} */ + _years = 1; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency2 = null; + + set years(value) { + this._years = value; + this._render(); + } + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._render(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._render(); + } + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name.includes("asset-or-currency-1")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-1"); + if (assetOrCurrency) { + this.assetOrCurrency1 = assetOrCurrency; + } + } else if (name.includes("asset-or-currency-2")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-2"); + if (assetOrCurrency) { + this.assetOrCurrency2 = assetOrCurrency; + } + } else if (name == 'years') { + this.years = parseInt(newValue); + } + } + + _assetOrCurrencyFromAttributes(prefix) { + const marketSize = this.getAttribute(`${prefix}-market-size`); + const growthRate = this.getAttribute(`${prefix}-growth-rate`); + if (marketSize && growthRate) { + return { marketSize: parseInt(marketSize), growthRate: parseInt(growthRate) }; + } + return null; + } + + _render() { + const nominator = this._exponentialNumberString(this._projectionNumerator()); + const denominator = this._exponentialNumberString(this._projectionDenominator()); + this.innerHTML = ` +
    ${nominator} / ${denominator} = ${this._projection()} +
    + `; + } + + _exponentialNumberString(n) { + return n?.toExponential(3) ?? ''; + } + + _marketSizeChangedByRate(marketSize, growthRate, decrease = false) { + let changedMarketSize; + if (decrease) { + changedMarketSize = marketSize - (marketSize * growthRate / 100.0); + } else { + changedMarketSize = marketSize + (marketSize * growthRate / 100.0); + } + if (changedMarketSize <= 0 || Number.isNaN(changedMarketSize)) { + return null; + } + return changedMarketSize; + } + + _marketSizeChangedByRateInGivenYears(marketSize, growthRate, years) { + const negativeYears = years < 0; + let increasedMarketSize = marketSize; + for (let i = 0; i < Math.abs(years); i++) { + increasedMarketSize = this._marketSizeChangedByRate(increasedMarketSize, growthRate, negativeYears); + if (!increasedMarketSize) { + return null; + } + } + return increasedMarketSize; + } + + _projectionNumerator() { + if (this._assetOrCurrency1) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency1.marketSize, + this._assetOrCurrency1.growthRate, + this._years); + } + return null; + } + + _projectionDenominator() { + if (this._assetOrCurrency2) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency2.marketSize, + this._assetOrCurrency2.growthRate, + this._years); + } + return null; + } + + _projection() { + const numerator = this._projectionNumerator(); + const denominator = this._projectionDenominator(); + if (numerator && denominator) { + return Math.round(numerator * 1000 / denominator) / 1000.0; + } + return ''; + } +} + +export function register() { + customElements.define("projections-result", ProjectionsResult); + customElements.define("projections-calculator", ProjectionsCalculator); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/registry.js b/web-components-reuse/vue-app/src/components/lib/registry.js index f08ffc14..5bebb6c6 100644 --- a/web-components-reuse/vue-app/src/components/lib/registry.js +++ b/web-components-reuse/vue-app/src/components/lib/registry.js @@ -2,8 +2,10 @@ import * as AssetElement from './asset-element.js'; import * as CurrencyElement from './currency-element.js'; import * as TabsContainer from './tabs-container.js'; import * as DropDown from './drop-down.js'; -import * as MarketsComparatorInput from './markets-comparator-input.js'; -import * as Customizations from './customizations.js'; +import * as MarketsHeader from './markets-header.js'; +import * as AssetsAndCurrencies from './assets-and-currencies.js'; +import * as MarketsComparator from './markets-comparator.js'; +import * as ProjectionsCalculator from './projections-calculator.js'; export function registerComponents() { @@ -11,6 +13,8 @@ export function registerComponents() { CurrencyElement.register(); TabsContainer.register(); DropDown.register(); - MarketsComparatorInput.register(); - Customizations.register(); + MarketsHeader.register(); + AssetsAndCurrencies.register(); + MarketsComparator.register(); + ProjectionsCalculator.register(); } \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/tabs-container.js b/web-components-reuse/vue-app/src/components/lib/tabs-container.js index 8e977bbc..9f6e591e 100644 --- a/web-components-reuse/vue-app/src/components/lib/tabs-container.js +++ b/web-components-reuse/vue-app/src/components/lib/tabs-container.js @@ -53,6 +53,17 @@ class TabsContainer extends HTMLElement { } } +class TabHeader extends HTMLElement { + + connectedCallback() { + this.classList.add("text-2xl"); + this.classList.add("p-2"); + this.classList.add("cursor-pointer"); + this.classList.add("grow"); + } +} + export function register() { customElements.define('tabs-container', TabsContainer); + customElements.define('tab-header', TabHeader); } \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/utils.js b/web-components-reuse/vue-app/src/components/lib/utils.js index 5719e9e3..d239aca5 100644 --- a/web-components-reuse/vue-app/src/components/lib/utils.js +++ b/web-components-reuse/vue-app/src/components/lib/utils.js @@ -13,4 +13,7 @@ export function formatMoney(value, denomination) { return `${value.substring(0, zeros - 6)} ${value.substring(zeros - 6, zeros - 3)} ${value.substring(zeros - 3)} ${denomination}`; } return `${value} ${denomination}`; -} \ No newline at end of file +} + +// TODO +export const translations = {}; \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue b/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue new file mode 100644 index 00000000..52c19780 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue b/web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue similarity index 98% rename from web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue rename to web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue index bab99cf4..a0638903 100644 --- a/web-components-reuse/vue-app/src/components/MarketsComparatorInput.vue +++ b/web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue @@ -1,6 +1,6 @@ diff --git a/web-components-reuse/vue-app/src/components/lib/asset-element.js b/web-components-reuse/vue-app/src/components/lib/asset-element.js index db00ecdf..3fc1692b 100644 --- a/web-components-reuse/vue-app/src/components/lib/asset-element.js +++ b/web-components-reuse/vue-app/src/components/lib/asset-element.js @@ -1,6 +1,5 @@ import { formatMoney, BaseHTMLElement } from './base.js'; - class AssetElement extends BaseHTMLElement { /** @@ -49,7 +48,7 @@ class AssetElement extends BaseHTMLElement {
    ${this.translation("previous-market-size-label")}:${formatMoney(this._previousMarketSize, denomination)}
    -

    ${marketIsUp ? this.translation('market-up-by') : this.translation('market-down-by')} ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; } else { previousMarketSizeComponent = ``; } diff --git a/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js b/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js index cbd6292e..fd9d4807 100644 --- a/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js +++ b/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js @@ -1,4 +1,6 @@ -class AssetsAndCurrencies extends HTMLElement { +import { BaseHTMLElement } from "./base.js"; + +class AssetsAndCurrencies extends BaseHTMLElement { _assets = []; _assetsValueChangeReason = undefined; @@ -30,20 +32,22 @@ class AssetsAndCurrencies extends HTMLElement { connectedCallback() { this.innerHTML = ` - -
    - Assets - Currencies -
    -
    -
    - ${this._assetsHTML()} +
    + +
    + ${this.translation('assets-header')} + ${this.translation('currencies-header')}
    -
    - ${this._currenciesHTML()} +
    +
    + ${this._assetsHTML()} +
    +
    + ${this._currenciesHTML()} +
    -
    -
    `; + +
    `; const tabsBody = this.querySelector("[data-tabs-body]"); this._assetsContainer = tabsBody.children[0]; @@ -69,10 +73,10 @@ class AssetsAndCurrencies extends HTMLElement { market-size="${a.marketSize}" previous-market-size="${previousMarketSize}" denomination="${a.denomination}" value-change-reason="${this._assetsValueChangeReason}" - t-market-size-label="Market size" - t-previous-market-size-label="Previous market size" - t-market-up-by="UP by" - t-market-down-by="DOWN by"> + ${this.translationAttribute('market-size-label')} + ${this.translationAttribute('previous-market-size-label')} + ${this.translationAttribute('up-by-info')} + ${this.translationAttribute('down-by-info')}> `; }).join("\n"); } @@ -94,7 +98,11 @@ class AssetsAndCurrencies extends HTMLElement { return ` + denomination="${c.denomination}" + ${this.translationAttribute('daily-turnover-label')} + ${this.translationAttribute('yearly-turnover-label')} + ${this.translationAttribute('up-by-info')} + ${this.translationAttribute('down-by-info')}> `}) .join("\n"); } diff --git a/web-components-reuse/vue-app/src/components/lib/base.js b/web-components-reuse/vue-app/src/components/lib/base.js index 0a6b0fd0..71f9b0c9 100644 --- a/web-components-reuse/vue-app/src/components/lib/base.js +++ b/web-components-reuse/vue-app/src/components/lib/base.js @@ -17,11 +17,40 @@ export function formatMoney(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 attributeTranslation = this.getAttribute(`t-${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(key) : null; + 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 ""; } } \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/currency-element.js b/web-components-reuse/vue-app/src/components/lib/currency-element.js index 365fa0a7..203c778d 100644 --- a/web-components-reuse/vue-app/src/components/lib/currency-element.js +++ b/web-components-reuse/vue-app/src/components/lib/currency-element.js @@ -1,7 +1,6 @@ -// TODO: better styling & translations -import * as Utils from './base.js'; +import { formatMoney, BaseHTMLElement } from './base.js'; -class CurrencyElement extends HTMLElement { +class CurrencyElement extends BaseHTMLElement { /** * Supported attributes @@ -45,7 +44,7 @@ class CurrencyElement extends HTMLElement { marketPercentageDiff = Math.round((previousMarketSizeInt - currentMarketSizeInt) * 100 * 100 / currentMarketSizeInt) / 100.0; } previousMarketSizeComponent = ` -

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%

    `; +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%

    `; } else { previousMarketSizeComponent = ``; } @@ -54,10 +53,10 @@ class CurrencyElement extends HTMLElement {

    ${name}

    - Daily turnover:${Utils.formatMoney(marketSize, denomination)} + ${this.translation('daily-turnover-label')}:${formatMoney(marketSize, denomination)}
    - Yearly turnover:${Utils.formatMoney(`${365 * parseInt(marketSize)}`, denomination)} + ${this.translation('yearly-turnover-label')}:${formatMoney(`${365 * parseInt(marketSize)}`, denomination)}
    ${previousMarketSizeComponent}
    diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator.js index 148c5194..b206324b 100644 --- a/web-components-reuse/vue-app/src/components/lib/markets-comparator.js +++ b/web-components-reuse/vue-app/src/components/lib/markets-comparator.js @@ -1,10 +1,12 @@ +import { BaseHTMLElement } from "./base.js"; + /** * @typedef {Object} AssetOrCurrency * @property {string} name * @property {number} marketSize */ -class MarketsComparator extends HTMLElement { +class MarketsComparator extends BaseHTMLElement { static observedAttributes = ["asset-items", "currency-items"]; @@ -47,28 +49,28 @@ class MarketsComparator extends HTMLElement { this._render(); this._chosenMarketSizeChangedEventHandler = e => { - const { componentId, name, marketSize } = e.detail; + const { name, marketSize } = e.detail; if (e.target === this._fromMarketsComparatorInput) { this._fromMarketSize = marketSize; this._renderComparisonElementHTML(); - this.dispatchEvent(new CustomEvent("mc:from-market-size-changed", { + this.dispatchEvent(new CustomEvent("mc.from-market-size-changed", { bubbles: true, detail: { name, marketSize } })); } else if (e.target === this._toMarketsComparatorInput) { this._toMarketSize = marketSize; this._renderComparisonElementHTML(); - this.dispatchEvent(new CustomEvent("mc:to-market-size-changed", { + this.dispatchEvent(new CustomEvent("mc.to-market-size-changed", { bubbles: true, detail: { name, marketSize } })); } } - document.addEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + document.addEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); } disconnectedCallback() { - document.removeEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + document.removeEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); } attributeChangedCallback(name, oldValue, newValue) { @@ -81,10 +83,18 @@ class MarketsComparator extends HTMLElement { _render() { this.innerHTML = ` -
    - -
    to
    - +
    + + +
    ${this.translation('markets-to')}
    + +
    ${this._comparisonElementHTML()}
    `; @@ -144,7 +154,7 @@ function currenciesFromAttributes(element) { return currencies; } -class MarketsComparatorInput extends HTMLElement { +class MarketsComparatorInput extends BaseHTMLElement { static observedAttributes = ["asset-items", "currency-items"]; @@ -211,18 +221,18 @@ class MarketsComparatorInput extends HTMLElement { _dropDownHeaderHTML() { let marketSizeHTML; if (this._isAsset()) { - marketSizeHTML = `market size${this.translation('market-size-input-label')} - days turnover + ${this.translation('days-turnover-input-label')} `; } else { marketSizeHTML = ``; } return ` - ${this._assetOrCurrencyInput ?? 'Asset/Currency'} + ${this._assetOrCurrencyInput ?? this.translation('asset-or-currency-input-placeholder')} ${marketSizeHTML} `; } @@ -273,7 +283,7 @@ class MarketsComparatorInput extends HTMLElement { const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; - this.dispatchEvent(new CustomEvent("mci:chosen-market-size-changed", { + this.dispatchEvent(new CustomEvent("mci.chosen-market-size-changed", { bubbles: true, detail: { name: this._assetOrCurrencyInput, marketSize: inputMarketSize } })); diff --git a/web-components-reuse/vue-app/src/components/lib/markets-header.js b/web-components-reuse/vue-app/src/components/lib/markets-header.js index cd9e43b1..ee620453 100644 --- a/web-components-reuse/vue-app/src/components/lib/markets-header.js +++ b/web-components-reuse/vue-app/src/components/lib/markets-header.js @@ -1,4 +1,6 @@ -class MarketsHeader extends HTMLElement { +import { BaseHTMLElement } from "./base"; + +class MarketsHeader extends BaseHTMLElement { _denomination = 'USD'; _liveUpdatesEnabled = true; @@ -21,15 +23,17 @@ class MarketsHeader extends HTMLElement { connectedCallback() { this.innerHTML = ` -
    Live Updates: ${this._liveUpdatesElementText()} +
    +
    ${this.translation('live-updates')} ${this._liveUpdatesElementText()} +
    + ${this.translation('markets-in')} + + ${this._denomination} +
      + ${this._denominationOptionsHTML()} +
    +
    - Markets in - - ${this._denomination} -
      - ${this._denominationOptionsHTML()} -
    -
    `; this._liveUpdatesEnabledElement = this.querySelector("span"); @@ -65,7 +69,7 @@ class MarketsHeader extends HTMLElement { } _liveUpdatesElementText() { - return `${this._liveUpdatesEnabled ? "ON" : "OFF"}`; + return `${this._liveUpdatesEnabled ? this.translation('live-updates-on') : this.translation('live-updates-off')}`; } } diff --git a/web-components-reuse/vue-app/src/components/lib/markets-projections.js b/web-components-reuse/vue-app/src/components/lib/markets-projections.js new file mode 100644 index 00000000..8010839a --- /dev/null +++ b/web-components-reuse/vue-app/src/components/lib/markets-projections.js @@ -0,0 +1,72 @@ +import { BaseHTMLElement } from "./base"; + +/** +* @typedef {Object} AssetOrCurrency +* @property {string} name +* @property {number} marketSize +*/ + +class MarketsProjections extends BaseHTMLElement { + + _marketsComparatorComponent = null; + _projectionsCalculatorComponent = null; + + /** @type {?AssetOrCurrency} */ + _fromAssetOrCurrency = null; + /** @type {?AssetOrCurrency} */ + _toAssetOrCurrency = null; + + /** @type {AssetOrCurrency[]} */ + set assets(value) { + if (this._marketsComparatorComponent) { + this._marketsComparatorComponent.assets = value; + } + } + + /** @type {AssetOrCurrency[]} */ + set currencies(value) { + if (this._marketsComparatorComponent) { + this._marketsComparatorComponent.currencies = value; + } + } + + connectedCallback() { + this.innerHTML = ` +
    +

    ${this.translation('projections-header')}

    + + + + +
    + `; + + this._marketsComparatorComponent = this.querySelector("markets-comparator"); + this._projectionsCalculatorComponent = this.querySelector("projections-calculator"); + + const fromMarketSizeChangedEventHandler = e => { + this._fromAssetOrCurrency = e.detail; + this._projectionsCalculatorComponent.assetOrCurrency1 = this._fromAssetOrCurrency; + }; + const toMarketSizeChangedEventHandler = e => { + this._toAssetOrCurrency = e.detail; + this._projectionsCalculatorComponent.assetOrCurrency2 = this._toAssetOrCurrency; + }; + + this.addEventListener('mc.from-market-size-changed', fromMarketSizeChangedEventHandler); + this.addEventListener('mc.to-market-size-changed', toMarketSizeChangedEventHandler); + } +} + +export function register() { + customElements.define('markets-projections', MarketsProjections); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/projections-calculator.js b/web-components-reuse/vue-app/src/components/lib/projections-calculator.js index c4fdcb57..367490b7 100644 --- a/web-components-reuse/vue-app/src/components/lib/projections-calculator.js +++ b/web-components-reuse/vue-app/src/components/lib/projections-calculator.js @@ -1,3 +1,5 @@ +import { BaseHTMLElement } from "./base.js"; + /** * @typedef {Object} AssetOrCurrency * @property {string} name @@ -8,7 +10,7 @@ * @property {number} growthRate */ -class ProjectionsCalculator extends HTMLElement { +class ProjectionsCalculator extends BaseHTMLElement { /** @type {?AssetOrCurrency} */ _assetOrCurrency1 = null; @@ -43,8 +45,7 @@ class ProjectionsCalculator extends HTMLElement { connectedCallback() { this.innerHTML = ` -

    Projections

    -
    +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    @@ -68,15 +69,14 @@ class ProjectionsCalculator extends HTMLElement { this._customProjectionInput] = container.querySelectorAll("input"); this._assetOrCurrency1Input.addEventListener("input", e => { - this._assetOrCurrency1ExpectedGrowthRate = e.target.value; + this._assetOrCurrency1ExpectedGrowthRate = parseInt(e.target.value); this._renderProjectionsResultsHTML(); }); this._assetOrCurrency2Input.addEventListener("input", e => { - this._assetOrCurrency2ExpectedGrowthRate = e.target.value; + this._assetOrCurrency2ExpectedGrowthRate = parseInt(e.target.value); this._renderProjectionsResultsHTML(); }); this._customProjectionInput.addEventListener("input", e => { - // TODO: is it really needed? this._customProjectionYears = parseInt(e.target.value); this._updateCustomProjectionText(); this._updateCustomProjectionResult(); @@ -87,7 +87,7 @@ class ProjectionsCalculator extends HTMLElement { } _assetOrCurrencyHeaderText(assetOrCurrency) { - return `${assetOrCurrency ? assetOrCurrency.name : "Asset/Currency"} expected annual growth rate:`; + return `${assetOrCurrency ? assetOrCurrency.name : this.translation('asset-or-currency-placeholder')} ${this.translation('asset-or-currency-expected-annual-growth-rate')}:`; } _renderProjectionsResultsHTML() { @@ -98,7 +98,7 @@ class ProjectionsCalculator extends HTMLElement { } _projectionsResultsHTML() { - const inYearsText = (years) => `In ${years} ${years == 1 ? 'year' : 'years'}`; + const inYearsText = (years) => `${this.translation('results-in-header')} ${years} ${this.translation(years == 1 ? 'year' : 'years')}`; const currentYear = new Date().getFullYear(); return [1, 5, 10].map(y => `
    ${inYearsText(y)} (${currentYear + y}):
    @@ -123,7 +123,7 @@ class ProjectionsCalculator extends HTMLElement { _customProjectionHTML() { return ` - In + ${this.translation('results-in-header')} ${this._customProjectionYearText()}:
    ${this._projectionsResultHTML(this._customProjectionYears)}
    `; @@ -132,7 +132,7 @@ class ProjectionsCalculator extends HTMLElement { _customProjectionYearText() { const currentYear = new Date().getFullYear(); return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? - `${currentYear} + N` : currentYear + this._customProjectionYears) + ')'; + `${currentYear} + N` : (currentYear + this._customProjectionYears)) + ')'; } _updateCustomProjectionText() { diff --git a/web-components-reuse/vue-app/src/components/lib/registry.js b/web-components-reuse/vue-app/src/components/lib/registry.js index 5bebb6c6..db7089ba 100644 --- a/web-components-reuse/vue-app/src/components/lib/registry.js +++ b/web-components-reuse/vue-app/src/components/lib/registry.js @@ -6,6 +6,7 @@ import * as MarketsHeader from './markets-header.js'; import * as AssetsAndCurrencies from './assets-and-currencies.js'; import * as MarketsComparator from './markets-comparator.js'; import * as ProjectionsCalculator from './projections-calculator.js'; +import * as MarketsProjections from './markets-projections.js'; export function registerComponents() { @@ -17,4 +18,5 @@ export function registerComponents() { AssetsAndCurrencies.register(); MarketsComparator.register(); ProjectionsCalculator.register(); + MarketsProjections.register(); } \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/tabs-container.js b/web-components-reuse/vue-app/src/components/lib/tabs-container.js index 9f6e591e..1095296a 100644 --- a/web-components-reuse/vue-app/src/components/lib/tabs-container.js +++ b/web-components-reuse/vue-app/src/components/lib/tabs-container.js @@ -8,12 +8,7 @@ class TabsContainer extends HTMLElement { activeTabClass = "underline"; - connectedCallback() { - this._render(); - } - - _render() { this._tabsHeader = this.querySelector("[data-tabs-header]"); this._tabsBody = this.querySelector("[data-tabs-body]"); this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; diff --git a/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue b/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue deleted file mode 100644 index 52c19780..00000000 --- a/web-components-reuse/vue-app/src/components/old/MarketsComparator.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue b/web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue deleted file mode 100644 index a0638903..00000000 --- a/web-components-reuse/vue-app/src/components/old/MarketsComparatorInput.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/old/ProjectionsCalculator.vue b/web-components-reuse/vue-app/src/components/old/ProjectionsCalculator.vue deleted file mode 100644 index 687176d7..00000000 --- a/web-components-reuse/vue-app/src/components/old/ProjectionsCalculator.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/old/ProjectionsResult.vue b/web-components-reuse/vue-app/src/components/old/ProjectionsResult.vue deleted file mode 100644 index 8293b87c..00000000 --- a/web-components-reuse/vue-app/src/components/old/ProjectionsResult.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/main.ts b/web-components-reuse/vue-app/src/main.ts index bb3e1009..58cb87d2 100644 --- a/web-components-reuse/vue-app/src/main.ts +++ b/web-components-reuse/vue-app/src/main.ts @@ -23,7 +23,38 @@ const i18n = createI18n({ fallbackLocale: 'en', messages: { en: { - calculatorHeader: 'Calculator' + 'markets-header': { + 'markets-in': "Market in", + 'live-updates': "Live updates:", + 'live-updates-on': "ON", + 'live-updates-off': "OFF" + }, + 'assets-and-currencies': { + 'assets-header': "Assets", + 'currencies-header': "Currencies", + 'market-size-label': "Market size", + 'previous-market-size-label': "Previous market size", + 'up-by-info': "UP by", + 'down-by-info': "DOWN by", + 'daily-turnover-label': "Daily turnover", + 'yearly-turnover-label': "Yearly turnover" + }, + 'markets-projections': { + 'projections-header': 'Projections', + 'markets-comparator': { + 'asset-or-currency-input-placeholder': 'Asset/Currency', + 'market-size-input-label': 'market size', + 'days-turnover-input-label': 'days turnover', + 'markets-to': 'to' + }, + 'projections-calculator': { + 'asset-or-currency-placeholder': 'Asset/Currency', + 'asset-or-currency-expected-annual-growth-rate': 'expected annual growth rate', + 'results-in-header': 'In', + 'year': 'year', + 'years': 'years' + } + } } } }); From cc0f2271a62e9ec786aaabbfdb18c8c1393e1969 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 4 Oct 2025 14:58:24 +0200 Subject: [PATCH 06/13] WebComponents: package --- .gitignore | 7 +- web-components-reuse/components/config.json | 17 + web-components-reuse/components/package.py | 45 + .../components/src/asset-element.js | 24 +- .../components/src/assets-and-currencies.js | 53 +- .../components/lib => components/src}/base.js | 7 + .../components/src/currency-element.js | 22 +- .../components/src/markets-comparator.js | 105 +- .../components/src/markets-header.js | 30 +- .../src}/markets-projections.js | 8 +- .../components/src/projections-calculator.js | 39 +- .../components/src/registry.js | 20 - .../components/src/tabs-container.js | 5 - web-components-reuse/components/src/utils.js | 19 - web-components-reuse/vue-app/.gitignore | 24 - .../src/components/lib/asset-element.js | 70 - .../components/lib/assets-and-currencies.js | 127 -- .../src/components/lib/currency-element.js | 69 - .../vue-app/src/components/lib/drop-down.js | 31 - .../src/components/lib/markets-comparator.js | 296 ----- .../src/components/lib/markets-header.js | 78 -- .../components/lib/projections-calculator.js | 291 ----- .../vue-app/src/components/lib/registry.js | 22 - .../src/components/lib/tabs-container.js | 64 - .../vue-app/src/components/web-components.js | 1135 +++++++++++++++++ web-components-reuse/vue-app/src/data/api.ts | 2 + web-components-reuse/vue-app/src/main.ts | 4 +- 27 files changed, 1371 insertions(+), 1243 deletions(-) create mode 100644 web-components-reuse/components/config.json create mode 100644 web-components-reuse/components/package.py rename web-components-reuse/{vue-app/src/components/lib => components/src}/base.js (94%) rename web-components-reuse/{vue-app/src/components/lib => components/src}/markets-projections.js (95%) delete mode 100644 web-components-reuse/components/src/registry.js delete mode 100644 web-components-reuse/components/src/utils.js delete mode 100644 web-components-reuse/vue-app/.gitignore delete mode 100644 web-components-reuse/vue-app/src/components/lib/asset-element.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/currency-element.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/drop-down.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/markets-comparator.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/markets-header.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/projections-calculator.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/registry.js delete mode 100644 web-components-reuse/vue-app/src/components/lib/tabs-container.js create mode 100644 web-components-reuse/vue-app/src/components/web-components.js diff --git a/.gitignore b/.gitignore index a10bd926..a697371f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,9 @@ out/ # Python **/__pycache__/ -**/venv/ \ No newline at end of file +**/venv/ + +# Logs +logs +*.log +npm-debug.log* \ No newline at end of file diff --git a/web-components-reuse/components/config.json b/web-components-reuse/components/config.json new file mode 100644 index 00000000..1597e884 --- /dev/null +++ b/web-components-reuse/components/config.json @@ -0,0 +1,17 @@ +{ + "components": [ + "base.js", + "tabs-container.js", + "drop-down.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" + ] +} \ No newline at end of file diff --git a/web-components-reuse/components/package.py b/web-components-reuse/components/package.py new file mode 100644 index 00000000..6c9baebb --- /dev/null +++ b/web-components-reuse/components/package.py @@ -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 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('}') diff --git a/web-components-reuse/components/src/asset-element.js b/web-components-reuse/components/src/asset-element.js index c0b25da7..3fc1692b 100644 --- a/web-components-reuse/components/src/asset-element.js +++ b/web-components-reuse/components/src/asset-element.js @@ -1,11 +1,17 @@ -// TODO: better styling & translations -import * as Utils from './utils.js'; +import { formatMoney, BaseHTMLElement } from './base.js'; -class AssetElement extends HTMLElement { - - static observedAttributes = ["market-size", "denomination", "value-change-reason"]; - _previousMarketSize = null; +class AssetElement extends BaseHTMLElement { + /** + * Supported attributes + * {string} id: asset id + * {string} name: asset name + * {number} market-size + * {number} previous-market-size + * {string} value-change-reason: optional reason of the market size change + * {string} denomination + * {string} class: additional class to append to the root div + */ connectedCallback() { this._render(); } @@ -40,9 +46,9 @@ class AssetElement extends HTMLElement { } previousMarketSizeComponent = `
    - Previous market size:${Utils.formatMoney(this._previousMarketSize, denomination)} + ${this.translation("previous-market-size-label")}:${formatMoney(this._previousMarketSize, denomination)}
    -

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; } else { previousMarketSizeComponent = ``; } @@ -51,7 +57,7 @@ class AssetElement extends HTMLElement {

    ${name}

    - Market size:${Utils.formatMoney(marketSize, denomination)} + ${this.translation('market-size-label')}:${formatMoney(marketSize, denomination)}
    ${previousMarketSizeComponent}
    diff --git a/web-components-reuse/components/src/assets-and-currencies.js b/web-components-reuse/components/src/assets-and-currencies.js index 4cb84999..c127fbd1 100644 --- a/web-components-reuse/components/src/assets-and-currencies.js +++ b/web-components-reuse/components/src/assets-and-currencies.js @@ -1,4 +1,14 @@ -class AssetsAndCurrencies extends HTMLElement { +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 = undefined; @@ -7,6 +17,7 @@ class AssetsAndCurrencies extends HTMLElement { _assetsContainer = undefined; _currenciesContainer = undefined; + /** @type {AssetOrCurrencyElement[]} */ set assets(value) { this._assets = value; this._renderAssets(); @@ -17,11 +28,13 @@ class AssetsAndCurrencies extends HTMLElement { this._renderAssets(); } + /** @type {AssetOrCurrencyElement[]} */ set currencies(value) { this._currencies = value; this._renderCurrencies(); } + /** @type {string} */ set denomination(value) { this._denomination = value; this._renderAssets(); @@ -30,20 +43,22 @@ class AssetsAndCurrencies extends HTMLElement { connectedCallback() { this.innerHTML = ` - -
    - Assets - Currencies -
    -
    -
    - ${this._assetsHTML()} +
    + +
    + ${this.translation('assets-header')} + ${this.translation('currencies-header')}
    -
    - ${this._currenciesHTML()} +
    +
    + ${this._assetsHTML()} +
    +
    + ${this._currenciesHTML()} +
    -
    -
    `; + +
    `; const tabsBody = this.querySelector("[data-tabs-body]"); this._assetsContainer = tabsBody.children[0]; @@ -68,7 +83,11 @@ class AssetsAndCurrencies extends HTMLElement { return ` + 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')}> `; }).join("\n"); } @@ -90,7 +109,11 @@ class AssetsAndCurrencies extends HTMLElement { return ` + denomination="${c.denomination}" + ${this.translationAttribute('daily-turnover-label')} + ${this.translationAttribute('yearly-turnover-label')} + ${this.translationAttribute('up-by-info')} + ${this.translationAttribute('down-by-info')}> `}) .join("\n"); } diff --git a/web-components-reuse/vue-app/src/components/lib/base.js b/web-components-reuse/components/src/base.js similarity index 94% rename from web-components-reuse/vue-app/src/components/lib/base.js rename to web-components-reuse/components/src/base.js index 71f9b0c9..861e2810 100644 --- a/web-components-reuse/vue-app/src/components/lib/base.js +++ b/web-components-reuse/components/src/base.js @@ -1,3 +1,10 @@ +// Common types definition +/** +* @typedef {Object} AssetOrCurrency +* @property {string} name +* @property {number} marketSize +*/ + export function formatMoney(value, denomination) { const zeros = value.length; if (zeros > 15) { diff --git a/web-components-reuse/components/src/currency-element.js b/web-components-reuse/components/src/currency-element.js index 362103f3..203c778d 100644 --- a/web-components-reuse/components/src/currency-element.js +++ b/web-components-reuse/components/src/currency-element.js @@ -1,10 +1,16 @@ -// TODO: better styling & translations -import * as Utils from './utils.js'; +import { formatMoney, BaseHTMLElement } from './base.js'; -class CurrencyElement extends HTMLElement { - - static observedAttributes = ["market-size", "denomination"]; +class CurrencyElement extends BaseHTMLElement { + /** + * Supported attributes + * {string} id: currency id + * {string} name: currency name + * {number} market-size + * {number} previous-market-size + * {string} denomination + * {string} class: additional class to append to the root div + */ connectedCallback() { this._render(); } @@ -38,7 +44,7 @@ class CurrencyElement extends HTMLElement { marketPercentageDiff = Math.round((previousMarketSizeInt - currentMarketSizeInt) * 100 * 100 / currentMarketSizeInt) / 100.0; } previousMarketSizeComponent = ` -

    ${marketIsUp ? 'UP' : 'DOWN'} by ${marketPercentageDiff}%

    `; +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%

    `; } else { previousMarketSizeComponent = ``; } @@ -47,10 +53,10 @@ class CurrencyElement extends HTMLElement {

    ${name}

    - Daily turnover:${Utils.formatMoney(marketSize, denomination)} + ${this.translation('daily-turnover-label')}:${formatMoney(marketSize, denomination)}
    - Yearly turnover:${Utils.formatMoney(`${365 * parseInt(marketSize)}`, denomination)} + ${this.translation('yearly-turnover-label')}:${formatMoney(`${365 * parseInt(marketSize)}`, denomination)}
    ${previousMarketSizeComponent}
    diff --git a/web-components-reuse/components/src/markets-comparator.js b/web-components-reuse/components/src/markets-comparator.js index 1efab493..e4d55e68 100644 --- a/web-components-reuse/components/src/markets-comparator.js +++ b/web-components-reuse/components/src/markets-comparator.js @@ -1,32 +1,33 @@ -// TODO: document types! -class MarketsComparator extends HTMLElement { +import { BaseHTMLElement, AssetOrCurrency } from "./base.js"; - static observedAttributes = ["asset-items", "currency-items"]; +class MarketsComparator extends BaseHTMLElement { - _fromInputId = crypto.randomUUID(); - _toInputId = crypto.randomUUID(); + static observedAttributes = ["asset-items", "currency-items"]; - _marketsComparatorInputFrom = null; - _marketsComparatorInputTo = null; + _fromMarketsComparatorInput = null; + _toMarketsComparatorInput = null; + _comparisonElement = null; _fromMarketSize = null; _toMarketSize = null; + /** @type {AssetOrCurrency[]} */ set assets(value) { - if (this._marketsComparatorInputFrom) { - this._setValuesUsingAttributes(this._marketsComparatorInputFrom, value, "asset"); + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "asset"); } - if (this._marketsComparatorInputTo) { - this._setValuesUsingAttributes(this._marketsComparatorInputTo, value, "asset"); + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "asset"); } } + /** @type {AssetOrCurrency[]} */ set currencies(value) { - if (this._marketsComparatorInputFrom) { - this._setValuesUsingAttributes(this._marketsComparatorInputFrom, value, "currency"); + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "currency"); } - if (this._marketsComparatorInputTo) { - this._setValuesUsingAttributes(this._marketsComparatorInputTo, value, "currency"); + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "currency"); } } @@ -42,32 +43,28 @@ class MarketsComparator extends HTMLElement { this._render(); this._chosenMarketSizeChangedEventHandler = e => { - const { componentId, name, marketSize } = e.detail; - if (componentId == this._fromInputId) { + const { name, marketSize } = e.detail; + if (e.target === this._fromMarketsComparatorInput) { this._fromMarketSize = marketSize; this._renderComparisonElementHTML(); - document.dispatchEvent(new CustomEvent("mc:from-market-size-changed", { - detail: { - componentId: this.id, - name, marketSize - } + this.dispatchEvent(new CustomEvent("mc.from-market-size-changed", { + bubbles: true, + detail: { name, marketSize } })); - } else if (componentId == this._toInputId) { + } else if (e.target === this._toMarketsComparatorInput) { this._toMarketSize = marketSize; this._renderComparisonElementHTML(); - document.dispatchEvent(new CustomEvent("mc:to-market-size-changed", { - detail: { - componentId: this.id, - name, marketSize - } + this.dispatchEvent(new CustomEvent("mc.to-market-size-changed", { + bubbles: true, + detail: { name, marketSize } })); } } - document.addEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + document.addEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); } disconnectedCallback() { - document.removeEventListener('mci:chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + document.removeEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); } attributeChangedCallback(name, oldValue, newValue) { @@ -79,21 +76,25 @@ class MarketsComparator extends HTMLElement { } _render() { - this._fromInputId = crypto.randomUUID(); - this._toInputId = crypto.randomUUID(); - this._comparisonElementId = crypto.randomUUID(); - this.innerHTML = ` -
    - -
    to
    - -
    ${this._comparisonElementHTML()}
    +
    + + +
    ${this.translation('markets-to')}
    + + +
    ${this._comparisonElementHTML()}
    `; - this._marketsComparatorInputFrom = document.getElementById(this._fromInputId); - this._marketsComparatorInputTo = document.getElementById(this._toInputId); + [this._fromMarketsComparatorInput, this._toMarketsComparatorInput] = this.querySelectorAll("markets-comparator-input"); + this._comparisonElement = this.querySelector('[data-comparison-element]'); } _comparisonElementHTML() { @@ -111,7 +112,7 @@ class MarketsComparator extends HTMLElement { } _renderComparisonElementHTML() { - document.getElementById(this._comparisonElementId).innerHTML = this._comparisonElementHTML(); + this._comparisonElement.innerHTML = this._comparisonElementHTML(); } } @@ -147,12 +148,15 @@ function currenciesFromAttributes(element) { return currencies; } -class MarketsComparatorInput extends HTMLElement { +class MarketsComparatorInput extends BaseHTMLElement { static observedAttributes = ["asset-items", "currency-items"]; + /** @type {AssetOrCurrency[]} */ _assets = []; + /** @type {AssetOrCurrency[]} */ _currencies = []; + /** @type {AssetOrCurrency[]} */ _assetOrCurrencyOptions = []; set assets(value) { @@ -211,18 +215,18 @@ class MarketsComparatorInput extends HTMLElement { _dropDownHeaderHTML() { let marketSizeHTML; if (this._isAsset()) { - marketSizeHTML = `market size${this.translation('market-size-input-label')} - days turnover + ${this.translation('days-turnover-input-label')} `; } else { marketSizeHTML = ``; } return ` - ${this._assetOrCurrencyInput ?? 'Asset/Currency'} + ${this._assetOrCurrencyInput ?? this.translation('asset-or-currency-input-placeholder')} ${marketSizeHTML} `; } @@ -273,12 +277,9 @@ class MarketsComparatorInput extends HTMLElement { const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; - document.dispatchEvent(new CustomEvent("mci:chosen-market-size-changed", { - detail: { - componentId: this.id, - name: this._assetOrCurrencyInput, - marketSize: inputMarketSize - } + this.dispatchEvent(new CustomEvent("mci.chosen-market-size-changed", { + bubbles: true, + detail: { name: this._assetOrCurrencyInput, marketSize: inputMarketSize } })); } } diff --git a/web-components-reuse/components/src/markets-header.js b/web-components-reuse/components/src/markets-header.js index 655c1049..b78f6e93 100644 --- a/web-components-reuse/components/src/markets-header.js +++ b/web-components-reuse/components/src/markets-header.js @@ -1,10 +1,12 @@ -class MarketsHeader extends HTMLElement { +import { BaseHTMLElement } from "./base.js"; + +class MarketsHeader extends BaseHTMLElement { _denomination = 'USD'; _liveUpdatesEnabled = true; _denominationExchangeRates = []; - _liveUpdatesEnabledElement = undefined; - _denominationElement = undefined; + _liveUpdatesEnabledElement = null; + _denominationElement = null; set denomination(value) { @@ -21,15 +23,17 @@ class MarketsHeader extends HTMLElement { connectedCallback() { this.innerHTML = ` -
    Live Updates: ${this._liveUpdatesElementText()} +
    +
    ${this.translation('live-updates')} ${this._liveUpdatesElementText()} +
    + ${this.translation('markets-in')} + + ${this._denomination} +
      + ${this._denominationOptionsHTML()} +
    +
    - Markets in - - ${this._denomination} -
      - ${this._denominationOptionsHTML()} -
    -
    `; this._liveUpdatesEnabledElement = this.querySelector("span"); @@ -59,13 +63,13 @@ class MarketsHeader extends HTMLElement { o.onclick = () => { this._denomination = o.getAttribute("data-option-id"); this._denominationElement.textContent = this._denomination; - document.dispatchEvent(new CustomEvent('mh:denomination-changed', { detail: this._denomination })); + this.dispatchEvent(new CustomEvent('mh:denomination-changed', { bubbles: true, detail: this._denomination })); }; }); } _liveUpdatesElementText() { - return `${this._liveUpdatesEnabled ? "ON" : "OFF"}`; + return `${this._liveUpdatesEnabled ? this.translation('live-updates-on') : this.translation('live-updates-off')}`; } } diff --git a/web-components-reuse/vue-app/src/components/lib/markets-projections.js b/web-components-reuse/components/src/markets-projections.js similarity index 95% rename from web-components-reuse/vue-app/src/components/lib/markets-projections.js rename to web-components-reuse/components/src/markets-projections.js index 8010839a..6a278bde 100644 --- a/web-components-reuse/vue-app/src/components/lib/markets-projections.js +++ b/web-components-reuse/components/src/markets-projections.js @@ -1,10 +1,4 @@ -import { BaseHTMLElement } from "./base"; - -/** -* @typedef {Object} AssetOrCurrency -* @property {string} name -* @property {number} marketSize -*/ +import { BaseHTMLElement, AssetOrCurrency } from "./base.js"; class MarketsProjections extends BaseHTMLElement { diff --git a/web-components-reuse/components/src/projections-calculator.js b/web-components-reuse/components/src/projections-calculator.js index 9887b01e..0dfda12f 100644 --- a/web-components-reuse/components/src/projections-calculator.js +++ b/web-components-reuse/components/src/projections-calculator.js @@ -1,14 +1,12 @@ +import { BaseHTMLElement, AssetOrCurrency } from "./base.js"; + /** -* @typedef {Object} AssetOrCurrency -* @property {string} name -* @property {number} marketSize -* * @typedef {Object} AssetOrCurrencyProjection * @property {number} marketSize * @property {number} growthRate */ -class ProjectionsCalculator extends HTMLElement { +class ProjectionsCalculator extends BaseHTMLElement { /** @type {?AssetOrCurrency} */ _assetOrCurrency1 = null; @@ -38,13 +36,12 @@ class ProjectionsCalculator extends HTMLElement { _customProjectionInput = null; _projectionsResultsContainer = null; _customProjectionContainer = null; - _customProjectionTextElementId = crypto.randomUUID(); - _customProjectionResultContainerId = crypto.randomUUID(); + _customProjectionTextElement = null; + _customProjectionResultContainer = null; connectedCallback() { this.innerHTML = ` -

    Projections

    -
    +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    @@ -68,23 +65,25 @@ class ProjectionsCalculator extends HTMLElement { this._customProjectionInput] = container.querySelectorAll("input"); this._assetOrCurrency1Input.addEventListener("input", e => { - this._assetOrCurrency1ExpectedGrowthRate = e.target.value; + this._assetOrCurrency1ExpectedGrowthRate = parseInt(e.target.value); this._renderProjectionsResultsHTML(); }); this._assetOrCurrency2Input.addEventListener("input", e => { - this._assetOrCurrency2ExpectedGrowthRate = e.target.value; + this._assetOrCurrency2ExpectedGrowthRate = parseInt(e.target.value); this._renderProjectionsResultsHTML(); }); this._customProjectionInput.addEventListener("input", e => { - // TODO: is it really needed? this._customProjectionYears = parseInt(e.target.value); this._updateCustomProjectionText(); this._updateCustomProjectionResult(); }); + + this._customProjectionTextElement = this.querySelector('[data-custom-projection-text-element]'); + this._customProjectionResultContainer = this.querySelector('[data-custom-projection-result-container]'); } _assetOrCurrencyHeaderText(assetOrCurrency) { - return `${assetOrCurrency ? assetOrCurrency.name : "Asset/Currency"} expected annual growth rate:`; + return `${assetOrCurrency ? assetOrCurrency.name : this.translation('asset-or-currency-placeholder')} ${this.translation('asset-or-currency-expected-annual-growth-rate')}:`; } _renderProjectionsResultsHTML() { @@ -95,7 +94,7 @@ class ProjectionsCalculator extends HTMLElement { } _projectionsResultsHTML() { - const inYearsText = (years) => `In ${years} ${years == 1 ? 'year' : 'years'}`; + const inYearsText = (years) => `${this.translation('results-in-header')} ${years} ${this.translation(years == 1 ? 'year' : 'years')}`; const currentYear = new Date().getFullYear(); return [1, 5, 10].map(y => `
    ${inYearsText(y)} (${currentYear + y}):
    @@ -120,24 +119,24 @@ class ProjectionsCalculator extends HTMLElement { _customProjectionHTML() { return ` - In - ${this._customProjectionYearText()}: -
    ${this._projectionsResultHTML(this._customProjectionYears)}
    + ${this.translation('results-in-header')} + ${this._customProjectionYearText()}: +
    ${this._projectionsResultHTML(this._customProjectionYears)}
    `; } _customProjectionYearText() { const currentYear = new Date().getFullYear(); return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? - `${currentYear} + N` : currentYear + this._customProjectionYears) + ')'; + `${currentYear} + N` : (currentYear + this._customProjectionYears)) + ')'; } _updateCustomProjectionText() { - document.getElementById(this._customProjectionTextElementId).textContent = this._customProjectionYearText(); + this._customProjectionTextElement.textContent = this._customProjectionYearText(); } _updateCustomProjectionResult() { - document.getElementById(this._customProjectionResultContainerId).innerHTML = this._projectionsResultHTML(this._customProjectionYears); + this._customProjectionContainer.innerHTML = this._projectionsResultHTML(this._customProjectionYears); } _assetOrCurrency1WithExpectedGrowthRate() { diff --git a/web-components-reuse/components/src/registry.js b/web-components-reuse/components/src/registry.js deleted file mode 100644 index 5bebb6c6..00000000 --- a/web-components-reuse/components/src/registry.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as AssetElement from './asset-element.js'; -import * as CurrencyElement from './currency-element.js'; -import * as TabsContainer from './tabs-container.js'; -import * as DropDown from './drop-down.js'; -import * as MarketsHeader from './markets-header.js'; -import * as AssetsAndCurrencies from './assets-and-currencies.js'; -import * as MarketsComparator from './markets-comparator.js'; -import * as ProjectionsCalculator from './projections-calculator.js'; - - -export function registerComponents() { - AssetElement.register(); - CurrencyElement.register(); - TabsContainer.register(); - DropDown.register(); - MarketsHeader.register(); - AssetsAndCurrencies.register(); - MarketsComparator.register(); - ProjectionsCalculator.register(); -} \ No newline at end of file diff --git a/web-components-reuse/components/src/tabs-container.js b/web-components-reuse/components/src/tabs-container.js index 9f6e591e..1095296a 100644 --- a/web-components-reuse/components/src/tabs-container.js +++ b/web-components-reuse/components/src/tabs-container.js @@ -8,12 +8,7 @@ class TabsContainer extends HTMLElement { activeTabClass = "underline"; - connectedCallback() { - this._render(); - } - - _render() { this._tabsHeader = this.querySelector("[data-tabs-header]"); this._tabsBody = this.querySelector("[data-tabs-body]"); this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; diff --git a/web-components-reuse/components/src/utils.js b/web-components-reuse/components/src/utils.js deleted file mode 100644 index d239aca5..00000000 --- a/web-components-reuse/components/src/utils.js +++ /dev/null @@ -1,19 +0,0 @@ -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}`; -} - -// TODO -export const translations = {}; \ No newline at end of file diff --git a/web-components-reuse/vue-app/.gitignore b/web-components-reuse/vue-app/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/web-components-reuse/vue-app/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/web-components-reuse/vue-app/src/components/lib/asset-element.js b/web-components-reuse/vue-app/src/components/lib/asset-element.js deleted file mode 100644 index 3fc1692b..00000000 --- a/web-components-reuse/vue-app/src/components/lib/asset-element.js +++ /dev/null @@ -1,70 +0,0 @@ -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} value-change-reason: optional reason of the market size change - * {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, valueChangeReason] = [this.getAttribute("id"), this.getAttribute("name"), - this.getAttribute("market-size"), this.getAttribute("previous-market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reason") - ]; - if (id == undefined || name == undefined) { - return; - } - const classesToAppend = this.getAttribute("class"); - - if (previousMarketSize) { - this._previousMarketSize = previousMarketSize; - } - - let previousMarketSizeComponent; - if (this._previousMarketSize && this._previousMarketSize != marketSize) { - const previousMarketSizeInt = parseInt(this._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 = ` -
    - ${this.translation("previous-market-size-label")}:${formatMoney(this._previousMarketSize, denomination)} -
    -

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; - } else { - previousMarketSizeComponent = ``; - } - - this.innerHTML = ` -
    -

    ${name}

    -
    - ${this.translation('market-size-label')}:${formatMoney(marketSize, denomination)} -
    - ${previousMarketSizeComponent} -
    - `; - } -} - -export function register() { - customElements.define("asset-element", AssetElement); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js b/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js deleted file mode 100644 index fd9d4807..00000000 --- a/web-components-reuse/vue-app/src/components/lib/assets-and-currencies.js +++ /dev/null @@ -1,127 +0,0 @@ -import { BaseHTMLElement } from "./base.js"; - -class AssetsAndCurrencies extends BaseHTMLElement { - - _assets = []; - _assetsValueChangeReason = undefined; - _currencies = []; - _denomination = "USD"; - _assetsContainer = undefined; - _currenciesContainer = undefined; - - set assets(value) { - this._assets = value; - this._renderAssets(); - } - - set assetsValueChangeReason(value) { - this._assetsValueChangeReason = value; - this._renderAssets(); - } - - set currencies(value) { - this._currencies = value; - this._renderCurrencies(); - } - - set denomination(value) { - this._denomination = value; - this._renderAssets(); - this._renderCurrencies(); - } - - connectedCallback() { - this.innerHTML = ` -
    - -
    - ${this.translation('assets-header')} - ${this.translation('currencies-header')} -
    -
    -
    - ${this._assetsHTML()} -
    -
    - ${this._currenciesHTML()} -
    -
    -
    -
    `; - - 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 previousCurrencyMarketSize = previousAsset.getAttribute("market-size"); - if (previousCurrencyMarketSize != a.marketSize) { - previousMarketSize = previousCurrencyMarketSize; - } else { - previousMarketSize = previousAsset.getAttribute("previous-market-size"); - } - } - - return ` - `; - }).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 previousCurrencyMarketSize = previousCurrency.getAttribute("market-size"); - if (previousCurrencyMarketSize != c.marketSize) { - previousMarketSize = previousCurrencyMarketSize; - } else { - previousMarketSize = previousCurrency.getAttribute("previous-market-size"); - } - } - - return ` - `}) - .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); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/currency-element.js b/web-components-reuse/vue-app/src/components/lib/currency-element.js deleted file mode 100644 index 203c778d..00000000 --- a/web-components-reuse/vue-app/src/components/lib/currency-element.js +++ /dev/null @@ -1,69 +0,0 @@ -import { formatMoney, BaseHTMLElement } from './base.js'; - -class CurrencyElement extends BaseHTMLElement { - - /** - * Supported attributes - * {string} id: currency id - * {string} name: currency 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 == undefined || name == undefined) { - return; - } - const classesToAppend = this.getAttribute("class"); - - if (previousMarketSize) { - this._previousMarketSize = previousMarketSize; - } - - let previousMarketSizeComponent; - if (this._previousMarketSize && this._previousMarketSize != marketSize) { - const previousMarketSizeInt = parseInt(this._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 = ` -

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%

    `; - } else { - previousMarketSizeComponent = ``; - } - - this.innerHTML = ` -
    -

    ${name}

    -
    - ${this.translation('daily-turnover-label')}:${formatMoney(marketSize, denomination)} -
    -
    - ${this.translation('yearly-turnover-label')}:${formatMoney(`${365 * parseInt(marketSize)}`, denomination)} -
    - ${previousMarketSizeComponent} -
    - `; - } -} - -export function register() { - customElements.define("currency-element", CurrencyElement); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/drop-down.js b/web-components-reuse/vue-app/src/components/lib/drop-down.js deleted file mode 100644 index 653defac..00000000 --- a/web-components-reuse/vue-app/src/components/lib/drop-down.js +++ /dev/null @@ -1,31 +0,0 @@ -class DropDownContainer extends HTMLElement { - - connectedCallback() { - const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; - const anchor = this.querySelector('[data-drop-down-anchor]') ?? this; - anchor.style = "position: relative; display: inline-block"; - - const options = this.querySelector("[data-drop-down-options]"); - if (!options) { - throw new Error("Options must be defined and marked with data-drop-down-options attribute!"); - } - options.style = `position: absolute; z-index: ${optionsZIndex}`; - options.classList.add("hidden"); - - anchor.onclick = (e) => { - // Do not hide other, opened DropDowns - e.stopPropagation(); - options.classList.toggle("hidden"); - }; - - window.addEventListener("click", e => { - if (e.target != anchor && e.target.parentNode != anchor) { - options.classList.add("hidden"); - } - }); - } -} - -export function register() { - customElements.define("drop-down-container", DropDownContainer); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-comparator.js b/web-components-reuse/vue-app/src/components/lib/markets-comparator.js deleted file mode 100644 index b206324b..00000000 --- a/web-components-reuse/vue-app/src/components/lib/markets-comparator.js +++ /dev/null @@ -1,296 +0,0 @@ -import { BaseHTMLElement } from "./base.js"; - -/** -* @typedef {Object} AssetOrCurrency -* @property {string} name -* @property {number} marketSize -*/ - -class MarketsComparator extends BaseHTMLElement { - - static observedAttributes = ["asset-items", "currency-items"]; - - _fromMarketsComparatorInput = null; - _toMarketsComparatorInput = null; - _comparisonElement = null; - - _fromMarketSize = null; - _toMarketSize = null; - - /** @type {AssetOrCurrency[]} */ - set assets(value) { - if (this._fromMarketsComparatorInput) { - this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "asset"); - } - if (this._toMarketsComparatorInput) { - this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "asset"); - } - } - - /** @type {AssetOrCurrency[]} */ - set currencies(value) { - if (this._fromMarketsComparatorInput) { - this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "currency"); - } - if (this._toMarketsComparatorInput) { - this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "currency"); - } - } - - _setValuesUsingAttributes(element, values, prefix) { - for (let i = 0; i < values.length; i++) { - element.setAttribute(`${prefix}-${i}-name`, values[i].name); - element.setAttribute(`${prefix}-${i}-market-size`, values[i].marketSize); - } - element.setAttribute(`${prefix}-items`, values.length); - } - - connectedCallback() { - this._render(); - - this._chosenMarketSizeChangedEventHandler = e => { - const { name, marketSize } = e.detail; - if (e.target === this._fromMarketsComparatorInput) { - this._fromMarketSize = marketSize; - this._renderComparisonElementHTML(); - this.dispatchEvent(new CustomEvent("mc.from-market-size-changed", { - bubbles: true, - detail: { name, marketSize } - })); - } else if (e.target === this._toMarketsComparatorInput) { - this._toMarketSize = marketSize; - this._renderComparisonElementHTML(); - this.dispatchEvent(new CustomEvent("mc.to-market-size-changed", { - bubbles: true, - detail: { name, marketSize } - })); - } - } - document.addEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); - } - - disconnectedCallback() { - document.removeEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); - } - - attributeChangedCallback(name, oldValue, newValue) { - if (name == 'asset-items') { - this.assets = assetsFromAttributes(this); - } else if (name == 'currency-items') { - this.currencies = currenciesFromAttributes(this); - } - } - - _render() { - this.innerHTML = ` -
    - - -
    ${this.translation('markets-to')}
    - - -
    ${this._comparisonElementHTML()}
    -
    - `; - - [this._fromMarketsComparatorInput, this._toMarketsComparatorInput] = this.querySelectorAll("markets-comparator-input"); - this._comparisonElement = this.querySelector('[data-comparison-element]'); - } - - _comparisonElementHTML() { - if (!this._fromMarketSize || !this._toMarketSize) { - return '-'; - } - return `${this._fromMarketSize.toExponential(3)} / ${this._toMarketSize.toExponential(3)} = ${this._chosenMarketsComparedValue()}`; - } - - _chosenMarketsComparedValue() { - if (!this._fromMarketSize || !this._toMarketSize) { - return 0; - } - return Math.round(this._fromMarketSize * 1000 / this._toMarketSize) / 1000.0; - } - - _renderComparisonElementHTML() { - this._comparisonElement.innerHTML = this._comparisonElementHTML(); - } -} - -function assetsFromAttributes(element) { - const countAttribute = element.getAttribute("asset-items"); - if (!countAttribute) { - return []; - } - const assets = []; - for (let i = 0; i < parseInt(countAttribute); i++) { - const name = element.getAttribute(`asset-${i}-name`); - const marketSize = element.getAttribute(`asset-${i}-market-size`); - if (name && marketSize) { - assets.push({ name, marketSize: parseInt(marketSize) }); - } - } - return assets; -} - -function currenciesFromAttributes(element) { - const countAttribute = element.getAttribute("currency-items"); - if (!countAttribute) { - return []; - } - const currencies = []; - for (let i = 0; i < parseInt(countAttribute); i++) { - const name = element.getAttribute(`currency-${i}-name`); - const marketSize = element.getAttribute(`currency-${i}-market-size`); - if (name && marketSize) { - currencies.push({ name, marketSize: parseInt(marketSize) }); - } - } - return currencies; -} - -class MarketsComparatorInput extends BaseHTMLElement { - - static observedAttributes = ["asset-items", "currency-items"]; - - /** @type {AssetOrCurrency[]} */ - _assets = []; - /** @type {AssetOrCurrency[]} */ - _currencies = []; - /** @type {AssetOrCurrency[]} */ - _assetOrCurrencyOptions = []; - - set assets(value) { - this._assets = value; - this._recalculateAssetOrCurrencyOptions(); - } - - set currencies(value) { - this._currencies = value; - this._recalculateAssetOrCurrencyOptions(); - } - - _assetOrCurrencyInput = null; - _curencyTurnoverInputMultiplier = 1; - - connectedCallback() { - this._render(); - } - - attributeChangedCallback(name, oldValue, newValue) { - if (name == 'asset-items') { - this.assets = assetsFromAttributes(this); - } else if (name == 'currency-items') { - this.currencies = currenciesFromAttributes(this); - } - } - - _render() { - const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; - this.innerHTML = ` - -
    - ${this._dropDownHeaderHTML()} -
    -
      - ${this._optionsHTML()} -
    -
    - `; - - this._setOptionsClickHandlers(); - - this.querySelector('[data-drop-down-header]') - .querySelector("input")?.addEventListener("input", e => { - this._curencyTurnoverInputMultiplier = e.target.value; - this._calculateChosenMarketSizeChange(); - }); - } - - _optionsHTML() { - return this._assetOrCurrencyOptions.map(o => - `
  • ${o.name}
  • `) - .join('\n'); - } - - _dropDownHeaderHTML() { - let marketSizeHTML; - if (this._isAsset()) { - marketSizeHTML = `${this.translation('market-size-input-label')} - - ${this.translation('days-turnover-input-label')} - `; - } else { - marketSizeHTML = ``; - } - return ` - ${this._assetOrCurrencyInput ?? this.translation('asset-or-currency-input-placeholder')} - ${marketSizeHTML} - `; - } - - _setOptionsClickHandlers() { - [...this.querySelectorAll("li")].forEach(o => { - o.onclick = () => { - this._assetOrCurrencyInput = o.getAttribute("data-option-id"); - this._calculateChosenMarketSizeChange(); - this._render(); - }; - }); - } - - _renderOptionsHTML() { - const optionsContainer = this.querySelector("ul"); - if (optionsContainer) { - optionsContainer.innerHTML = this._optionsHTML(); - this._setOptionsClickHandlers(); - } - } - - _isAsset() { - return this._assetOrCurrencyInput ? this._assets.find(a => a.name == this._assetOrCurrencyInput) : false; - } - - _isCurrency() { - return this._assetOrCurrencyInput ? this._currencies.find(c => c.name == this._assetOrCurrencyInput) : false; - } - - _recalculateAssetOrCurrencyOptions() { - const assetOrCurrencyOptions = []; - this._assets.forEach(a => assetOrCurrencyOptions.push(a)); - this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); - this._assetOrCurrencyOptions = assetOrCurrencyOptions; - - this._renderOptionsHTML(); - this._calculateChosenMarketSizeChange(); - } - - _calculateChosenMarketSizeChange() { - if (!this._assetOrCurrencyInput) { - return; - } - - const assetInput = this._assets.find(a => a.name == this._assetOrCurrencyInput); - const currencyInput = this._currencies.find(c => c.name == this._assetOrCurrencyInput); - - const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; - - this.dispatchEvent(new CustomEvent("mci.chosen-market-size-changed", { - bubbles: true, - detail: { name: this._assetOrCurrencyInput, marketSize: inputMarketSize } - })); - } -} - -export function register() { - customElements.define("markets-comparator-input", MarketsComparatorInput); - customElements.define('markets-comparator', MarketsComparator); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/markets-header.js b/web-components-reuse/vue-app/src/components/lib/markets-header.js deleted file mode 100644 index ee620453..00000000 --- a/web-components-reuse/vue-app/src/components/lib/markets-header.js +++ /dev/null @@ -1,78 +0,0 @@ -import { BaseHTMLElement } from "./base"; - -class MarketsHeader extends BaseHTMLElement { - - _denomination = 'USD'; - _liveUpdatesEnabled = true; - _denominationExchangeRates = []; - _liveUpdatesEnabledElement = null; - _denominationElement = null; - - - set denomination(value) { - this._denomination = value; - if (this._denominationElement) { - this._denominationElement = this._denomination; - } - } - - set denominationExchangeRates(value) { - this._denominationExchangeRates = value; - this._renderDenominationOptions(); - } - - connectedCallback() { - this.innerHTML = ` -
    -
    ${this.translation('live-updates')} ${this._liveUpdatesElementText()} -
    - ${this.translation('markets-in')} - - ${this._denomination} -
      - ${this._denominationOptionsHTML()} -
    -
    -
    - `; - - this._liveUpdatesEnabledElement = this.querySelector("span"); - this._liveUpdatesEnabledElement.onclick = () => { - this._liveUpdatesEnabled = !this._liveUpdatesEnabled; - this._liveUpdatesEnabledElement.textContent = this._liveUpdatesElementText(); - document.dispatchEvent(new CustomEvent('mh:live-updates-toggled', { detail: this._liveUpdatesEnabled })); - }; - - this._denominationElement = this.querySelector("drop-down-container > span"); - } - - _denominationOptionsHTML() { - return this._denominationExchangeRates.map(der => `
  • ${der.name}: ${der.exchangeRate}
  • `).join('\n'); - } - - _renderDenominationOptions() { - const optionsContainer = this.querySelector("ul"); - if (optionsContainer) { - optionsContainer.innerHTML = this._denominationOptionsHTML(); - this._setOptionsClickHandlers(); - } - } - - _setOptionsClickHandlers() { - [...this.querySelectorAll("li")].forEach(o => { - o.onclick = () => { - this._denomination = o.getAttribute("data-option-id"); - this._denominationElement.textContent = this._denomination; - document.dispatchEvent(new CustomEvent('mh:denomination-changed', { detail: this._denomination })); - }; - }); - } - - _liveUpdatesElementText() { - return `${this._liveUpdatesEnabled ? this.translation('live-updates-on') : this.translation('live-updates-off')}`; - } -} - -export function register() { - customElements.define("markets-header", MarketsHeader); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/projections-calculator.js b/web-components-reuse/vue-app/src/components/lib/projections-calculator.js deleted file mode 100644 index 367490b7..00000000 --- a/web-components-reuse/vue-app/src/components/lib/projections-calculator.js +++ /dev/null @@ -1,291 +0,0 @@ -import { BaseHTMLElement } from "./base.js"; - -/** -* @typedef {Object} AssetOrCurrency -* @property {string} name -* @property {number} marketSize -* -* @typedef {Object} AssetOrCurrencyProjection -* @property {number} marketSize -* @property {number} growthRate -*/ - -class ProjectionsCalculator extends BaseHTMLElement { - - /** @type {?AssetOrCurrency} */ - _assetOrCurrency1 = null; - /** @type {?AssetOrCurrency} */ - _assetOrCurrency2 = null; - /** @type {?number} */ - _assetOrCurrency1ExpectedGrowthRate = null; - /** @type {?number} */ - _assetOrCurrency2ExpectedGrowthRate = null; - /** @type {?number} */ - _customProjectionYears = null; - - set assetOrCurrency1(value) { - this._assetOrCurrency1 = value; - this._renderProjectionsResultsHTML(); - } - - set assetOrCurrency2(value) { - this._assetOrCurrency2 = value; - this._renderProjectionsResultsHTML(); - } - - _assetOrCurrency1Header = null; - _assetOrCurrency1Input = null; - _assetOrCurrency2Header = null; - _assetOrCurrency2Input = null; - _customProjectionInput = null; - _projectionsResultsContainer = null; - _customProjectionContainer = null; - _customProjectionTextElement = null; - _customProjectionResultContainer = null; - - connectedCallback() { - this.innerHTML = ` -
    -
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    - -
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    - -
    - ${this._projectionsResultsHTML()} -
    -
    - ${this._customProjectionHTML()} -
    -
    - `; - - const container = this.querySelector("div"); - - const divs = container.querySelectorAll("div"); - [this._assetOrCurrency1Header, this._assetOrCurrency2Header, this._projectionsResultsContainer] = divs; - this._customProjectionContainer = divs[divs.length - 1]; - - [this._assetOrCurrency1Input, this._assetOrCurrency2Input, - this._customProjectionInput] = container.querySelectorAll("input"); - - this._assetOrCurrency1Input.addEventListener("input", e => { - this._assetOrCurrency1ExpectedGrowthRate = parseInt(e.target.value); - this._renderProjectionsResultsHTML(); - }); - this._assetOrCurrency2Input.addEventListener("input", e => { - this._assetOrCurrency2ExpectedGrowthRate = parseInt(e.target.value); - this._renderProjectionsResultsHTML(); - }); - this._customProjectionInput.addEventListener("input", e => { - this._customProjectionYears = parseInt(e.target.value); - this._updateCustomProjectionText(); - this._updateCustomProjectionResult(); - }); - - this._customProjectionTextElement = this.querySelector('[data-custom-projection-text-element]'); - this._customProjectionResultContainer = this.querySelector('[data-custom-projection-result-container]'); - } - - _assetOrCurrencyHeaderText(assetOrCurrency) { - return `${assetOrCurrency ? assetOrCurrency.name : this.translation('asset-or-currency-placeholder')} ${this.translation('asset-or-currency-expected-annual-growth-rate')}:`; - } - - _renderProjectionsResultsHTML() { - if (this._projectionsResultsContainer && this._customProjectionContainer) { - this._projectionsResultsContainer.innerHTML = this._projectionsResultsHTML(); - this._updateCustomProjectionResult(); - } - } - - _projectionsResultsHTML() { - const inYearsText = (years) => `${this.translation('results-in-header')} ${years} ${this.translation(years == 1 ? 'year' : 'years')}`; - const currentYear = new Date().getFullYear(); - return [1, 5, 10].map(y => ` -
    ${inYearsText(y)} (${currentYear + y}):
    - ${this._projectionsResultHTML(y)}`) - .join('\n'); - } - - _projectionsResultHTML(years) { - const ac1 = this._assetOrCurrency1WithExpectedGrowthRate(); - const ac2 = this._assetOrCurrency2WithExpectedGrowthRate(); - if (years != null && ac1 != null && ac2 != null) { - return ` - - `; - } - return '
    -
    '; - } - - _customProjectionHTML() { - return ` - ${this.translation('results-in-header')} - ${this._customProjectionYearText()}: -
    ${this._projectionsResultHTML(this._customProjectionYears)}
    - `; - } - - _customProjectionYearText() { - const currentYear = new Date().getFullYear(); - return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? - `${currentYear} + N` : (currentYear + this._customProjectionYears)) + ')'; - } - - _updateCustomProjectionText() { - this._customProjectionTextElement.textContent = this._customProjectionYearText(); - } - - _updateCustomProjectionResult() { - this._customProjectionContainer.innerHTML = this._projectionsResultHTML(this._customProjectionYears); - } - - _assetOrCurrency1WithExpectedGrowthRate() { - if (this._assetOrCurrency1 && this._assetOrCurrency1ExpectedGrowthRate != null) { - return { marketSize: this._assetOrCurrency1.marketSize, growthRate: this._assetOrCurrency1ExpectedGrowthRate }; - } - return null; - } - - _assetOrCurrency2WithExpectedGrowthRate() { - if (this._assetOrCurrency2 && this._assetOrCurrency2ExpectedGrowthRate != null) { - return { marketSize: this._assetOrCurrency2.marketSize, growthRate: this._assetOrCurrency2ExpectedGrowthRate }; - } - return null; - } -} - -class ProjectionsResult extends HTMLElement { - - static observedAttributes = [ - "years", - "asset-or-currency-1-market-size", "asset-or-currency-1-growth-rate", - "asset-or-currency-2-market-size", "asset-or-currency-2-growth-rate" - ]; - - /** @type {number} */ - _years = 1; - /** @type {?AssetOrCurrencyProjection} */ - _assetOrCurrency1 = null; - /** @type {?AssetOrCurrencyProjection} */ - _assetOrCurrency2 = null; - - set years(value) { - this._years = value; - this._render(); - } - - set assetOrCurrency1(value) { - this._assetOrCurrency1 = value; - this._render(); - } - - set assetOrCurrency2(value) { - this._assetOrCurrency2 = value; - this._render(); - } - - connectedCallback() { - this._render(); - } - - attributeChangedCallback(name, oldValue, newValue) { - if (name.includes("asset-or-currency-1")) { - const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-1"); - if (assetOrCurrency) { - this.assetOrCurrency1 = assetOrCurrency; - } - } else if (name.includes("asset-or-currency-2")) { - const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-2"); - if (assetOrCurrency) { - this.assetOrCurrency2 = assetOrCurrency; - } - } else if (name == 'years') { - this.years = parseInt(newValue); - } - } - - _assetOrCurrencyFromAttributes(prefix) { - const marketSize = this.getAttribute(`${prefix}-market-size`); - const growthRate = this.getAttribute(`${prefix}-growth-rate`); - if (marketSize && growthRate) { - return { marketSize: parseInt(marketSize), growthRate: parseInt(growthRate) }; - } - return null; - } - - _render() { - const nominator = this._exponentialNumberString(this._projectionNumerator()); - const denominator = this._exponentialNumberString(this._projectionDenominator()); - this.innerHTML = ` -
    ${nominator} / ${denominator} = ${this._projection()} -
    - `; - } - - _exponentialNumberString(n) { - return n?.toExponential(3) ?? ''; - } - - _marketSizeChangedByRate(marketSize, growthRate, decrease = false) { - let changedMarketSize; - if (decrease) { - changedMarketSize = marketSize - (marketSize * growthRate / 100.0); - } else { - changedMarketSize = marketSize + (marketSize * growthRate / 100.0); - } - if (changedMarketSize <= 0 || Number.isNaN(changedMarketSize)) { - return null; - } - return changedMarketSize; - } - - _marketSizeChangedByRateInGivenYears(marketSize, growthRate, years) { - const negativeYears = years < 0; - let increasedMarketSize = marketSize; - for (let i = 0; i < Math.abs(years); i++) { - increasedMarketSize = this._marketSizeChangedByRate(increasedMarketSize, growthRate, negativeYears); - if (!increasedMarketSize) { - return null; - } - } - return increasedMarketSize; - } - - _projectionNumerator() { - if (this._assetOrCurrency1) { - return this._marketSizeChangedByRateInGivenYears( - this._assetOrCurrency1.marketSize, - this._assetOrCurrency1.growthRate, - this._years); - } - return null; - } - - _projectionDenominator() { - if (this._assetOrCurrency2) { - return this._marketSizeChangedByRateInGivenYears( - this._assetOrCurrency2.marketSize, - this._assetOrCurrency2.growthRate, - this._years); - } - return null; - } - - _projection() { - const numerator = this._projectionNumerator(); - const denominator = this._projectionDenominator(); - if (numerator && denominator) { - return Math.round(numerator * 1000 / denominator) / 1000.0; - } - return ''; - } -} - -export function register() { - customElements.define("projections-result", ProjectionsResult); - customElements.define("projections-calculator", ProjectionsCalculator); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/registry.js b/web-components-reuse/vue-app/src/components/lib/registry.js deleted file mode 100644 index db7089ba..00000000 --- a/web-components-reuse/vue-app/src/components/lib/registry.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as AssetElement from './asset-element.js'; -import * as CurrencyElement from './currency-element.js'; -import * as TabsContainer from './tabs-container.js'; -import * as DropDown from './drop-down.js'; -import * as MarketsHeader from './markets-header.js'; -import * as AssetsAndCurrencies from './assets-and-currencies.js'; -import * as MarketsComparator from './markets-comparator.js'; -import * as ProjectionsCalculator from './projections-calculator.js'; -import * as MarketsProjections from './markets-projections.js'; - - -export function registerComponents() { - AssetElement.register(); - CurrencyElement.register(); - TabsContainer.register(); - DropDown.register(); - MarketsHeader.register(); - AssetsAndCurrencies.register(); - MarketsComparator.register(); - ProjectionsCalculator.register(); - MarketsProjections.register(); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/lib/tabs-container.js b/web-components-reuse/vue-app/src/components/lib/tabs-container.js deleted file mode 100644 index 1095296a..00000000 --- a/web-components-reuse/vue-app/src/components/lib/tabs-container.js +++ /dev/null @@ -1,64 +0,0 @@ -class TabsContainer extends HTMLElement { - - _activeTab = 0; - set activeTab(value) { - this._activeTab = value; - this._updateActiveTab(); - } - - activeTabClass = "underline"; - - connectedCallback() { - this._tabsHeader = this.querySelector("[data-tabs-header]"); - this._tabsBody = this.querySelector("[data-tabs-body]"); - this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; - if (!this._tabsHeader) { - throw new Error("Tabs header must be defined and marked with data-tabs-header attribute!"); - } - if (!this._tabsBody) { - throw new Error("Tabs body must be defined and marked with data-tabs-body attribute!"); - } - - [...this._tabsHeader.children].forEach((tab, i) => { - tab.addEventListener('click', () => this.activeTab = i); - }); - - [...this._tabsBody.children].forEach((tab) => { - tab.classList.add("hidden"); - }); - - this._updateActiveTab(); - } - - _updateActiveTab() { - [...this._tabsHeader.children].forEach((tab, i) => { - if (i == this._activeTab) { - tab.classList.add(this.activeTabClass); - } else { - tab.classList.remove(this.activeTabClass); - } - }); - [...this._tabsBody.children].forEach((tab, i) => { - if (i == this._activeTab) { - tab.classList.remove('hidden'); - } else { - tab.classList.add('hidden'); - } - }); - } -} - -class TabHeader extends HTMLElement { - - connectedCallback() { - this.classList.add("text-2xl"); - this.classList.add("p-2"); - this.classList.add("cursor-pointer"); - this.classList.add("grow"); - } -} - -export function register() { - customElements.define('tabs-container', TabsContainer); - customElements.define('tab-header', TabHeader); -} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/components/web-components.js b/web-components-reuse/vue-app/src/components/web-components.js new file mode 100644 index 00000000..2fe0b810 --- /dev/null +++ b/web-components-reuse/vue-app/src/components/web-components.js @@ -0,0 +1,1135 @@ +// Generated from web-components-reuse:7f3883317c0f6e4ce0f588d82bac87b7f5771a74 + +// 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 ""; + } +}class TabsContainer extends HTMLElement { + + _activeTab = 0; + set activeTab(value) { + this._activeTab = value; + this._updateActiveTab(); + } + + activeTabClass = "underline"; + + connectedCallback() { + this._tabsHeader = this.querySelector("[data-tabs-header]"); + this._tabsBody = this.querySelector("[data-tabs-body]"); + this.activeTabClass = this.getAttribute("active-tab-class") ?? this.activeTabClass; + if (!this._tabsHeader) { + throw new Error("Tabs header must be defined and marked with data-tabs-header attribute!"); + } + if (!this._tabsBody) { + throw new Error("Tabs body must be defined and marked with data-tabs-body attribute!"); + } + + [...this._tabsHeader.children].forEach((tab, i) => { + tab.addEventListener('click', () => this.activeTab = i); + }); + + [...this._tabsBody.children].forEach((tab) => { + tab.classList.add("hidden"); + }); + + this._updateActiveTab(); + } + + _updateActiveTab() { + [...this._tabsHeader.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.add(this.activeTabClass); + } else { + tab.classList.remove(this.activeTabClass); + } + }); + [...this._tabsBody.children].forEach((tab, i) => { + if (i == this._activeTab) { + tab.classList.remove('hidden'); + } else { + tab.classList.add('hidden'); + } + }); + } +} + +class TabHeader extends HTMLElement { + + connectedCallback() { + this.classList.add("text-2xl"); + this.classList.add("p-2"); + this.classList.add("cursor-pointer"); + this.classList.add("grow"); + } +} + +class DropDownContainer extends HTMLElement { + + connectedCallback() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; + const anchor = this.querySelector('[data-drop-down-anchor]') ?? this; + anchor.style = "position: relative; display: inline-block"; + + const options = this.querySelector("[data-drop-down-options]"); + if (!options) { + throw new Error("Options must be defined and marked with data-drop-down-options attribute!"); + } + options.style = `position: absolute; z-index: ${optionsZIndex}`; + options.classList.add("hidden"); + + anchor.onclick = (e) => { + // Do not hide other, opened DropDowns + e.stopPropagation(); + options.classList.toggle("hidden"); + }; + + window.addEventListener("click", e => { + if (e.target != anchor && e.target.parentNode != anchor) { + options.classList.add("hidden"); + } + }); + } +} + + +class AssetElement extends BaseHTMLElement { + + /** + * Supported attributes + * {string} id: asset id + * {string} name: asset name + * {number} market-size + * {number} previous-market-size + * {string} value-change-reason: optional reason of the market size change + * {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, valueChangeReason] = [this.getAttribute("id"), this.getAttribute("name"), + this.getAttribute("market-size"), this.getAttribute("previous-market-size"), this.getAttribute("denomination"), this.getAttribute("value-change-reason") + ]; + if (id == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +
    + ${this.translation("previous-market-size-label")}:${formatMoney(this._previousMarketSize, denomination)} +
    +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%; ${valueChangeReason ?? 'UNKNOWN'}

    `; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
    +

    ${name}

    +
    + ${this.translation('market-size-label')}:${formatMoney(marketSize, denomination)} +
    + ${previousMarketSizeComponent} +
    + `; + } +} + + +class CurrencyElement extends BaseHTMLElement { + + /** + * Supported attributes + * {string} id: currency id + * {string} name: currency 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 == undefined || name == undefined) { + return; + } + const classesToAppend = this.getAttribute("class"); + + if (previousMarketSize) { + this._previousMarketSize = previousMarketSize; + } + + let previousMarketSizeComponent; + if (this._previousMarketSize && this._previousMarketSize != marketSize) { + const previousMarketSizeInt = parseInt(this._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 = ` +

    ${marketIsUp ? this.translation('up-by-info') : this.translation('down-by-info')} ${marketPercentageDiff}%

    `; + } else { + previousMarketSizeComponent = ``; + } + + this.innerHTML = ` +
    +

    ${name}

    +
    + ${this.translation('daily-turnover-label')}:${formatMoney(marketSize, denomination)} +
    +
    + ${this.translation('yearly-turnover-label')}:${formatMoney(`${365 * parseInt(marketSize)}`, denomination)} +
    + ${previousMarketSizeComponent} +
    + `; + } +} + + +/** +* @typedef {Object} AssetOrCurrencyElement +* @property {string} id +* @property {string} name +* @property {number} marketSize +* @property {string} denomination +*/ + +class AssetsAndCurrencies extends BaseHTMLElement { + + _assets = []; + _assetsValueChangeReason = undefined; + _currencies = []; + _denomination = "USD"; + _assetsContainer = undefined; + _currenciesContainer = undefined; + + /** @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 = ` +
    + +
    + ${this.translation('assets-header')} + ${this.translation('currencies-header')} +
    +
    +
    + ${this._assetsHTML()} +
    +
    + ${this._currenciesHTML()} +
    +
    +
    +
    `; + + 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 previousCurrencyMarketSize = previousAsset.getAttribute("market-size"); + if (previousCurrencyMarketSize != a.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousAsset.getAttribute("previous-market-size"); + } + } + + return ` + `; + }).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 previousCurrencyMarketSize = previousCurrency.getAttribute("market-size"); + if (previousCurrencyMarketSize != c.marketSize) { + previousMarketSize = previousCurrencyMarketSize; + } else { + previousMarketSize = previousCurrency.getAttribute("previous-market-size"); + } + } + + return ` + `}) + .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); + } + } +} + + +class MarketsHeader extends BaseHTMLElement { + + _denomination = 'USD'; + _liveUpdatesEnabled = true; + _denominationExchangeRates = []; + _liveUpdatesEnabledElement = null; + _denominationElement = null; + + + set denomination(value) { + this._denomination = value; + if (this._denominationElement) { + this._denominationElement = this._denomination; + } + } + + set denominationExchangeRates(value) { + this._denominationExchangeRates = value; + this._renderDenominationOptions(); + } + + connectedCallback() { + this.innerHTML = ` +
    +
    ${this.translation('live-updates')} ${this._liveUpdatesElementText()} +
    + ${this.translation('markets-in')} + + ${this._denomination} +
      + ${this._denominationOptionsHTML()} +
    +
    +
    + `; + + this._liveUpdatesEnabledElement = this.querySelector("span"); + this._liveUpdatesEnabledElement.onclick = () => { + this._liveUpdatesEnabled = !this._liveUpdatesEnabled; + this._liveUpdatesEnabledElement.textContent = this._liveUpdatesElementText(); + document.dispatchEvent(new CustomEvent('mh:live-updates-toggled', { detail: this._liveUpdatesEnabled })); + }; + + this._denominationElement = this.querySelector("drop-down-container > span"); + } + + _denominationOptionsHTML() { + return this._denominationExchangeRates.map(der => `
  • ${der.name}: ${der.exchangeRate}
  • `).join('\n'); + } + + _renderDenominationOptions() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._denominationOptionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._denomination = o.getAttribute("data-option-id"); + this._denominationElement.textContent = this._denomination; + document.dispatchEvent(new CustomEvent('mh:denomination-changed', { detail: this._denomination })); + }; + }); + } + + _liveUpdatesElementText() { + return `${this._liveUpdatesEnabled ? this.translation('live-updates-on') : this.translation('live-updates-off')}`; + } +} + + +class MarketsComparator extends BaseHTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + _fromMarketsComparatorInput = null; + _toMarketsComparatorInput = null; + _comparisonElement = null; + + _fromMarketSize = null; + _toMarketSize = null; + + /** @type {AssetOrCurrency[]} */ + set assets(value) { + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "asset"); + } + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "asset"); + } + } + + /** @type {AssetOrCurrency[]} */ + set currencies(value) { + if (this._fromMarketsComparatorInput) { + this._setValuesUsingAttributes(this._fromMarketsComparatorInput, value, "currency"); + } + if (this._toMarketsComparatorInput) { + this._setValuesUsingAttributes(this._toMarketsComparatorInput, value, "currency"); + } + } + + _setValuesUsingAttributes(element, values, prefix) { + for (let i = 0; i < values.length; i++) { + element.setAttribute(`${prefix}-${i}-name`, values[i].name); + element.setAttribute(`${prefix}-${i}-market-size`, values[i].marketSize); + } + element.setAttribute(`${prefix}-items`, values.length); + } + + connectedCallback() { + this._render(); + + this._chosenMarketSizeChangedEventHandler = e => { + const { name, marketSize } = e.detail; + if (e.target === this._fromMarketsComparatorInput) { + this._fromMarketSize = marketSize; + this._renderComparisonElementHTML(); + this.dispatchEvent(new CustomEvent("mc.from-market-size-changed", { + bubbles: true, + detail: { name, marketSize } + })); + } else if (e.target === this._toMarketsComparatorInput) { + this._toMarketSize = marketSize; + this._renderComparisonElementHTML(); + this.dispatchEvent(new CustomEvent("mc.to-market-size-changed", { + bubbles: true, + detail: { name, marketSize } + })); + } + } + document.addEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + disconnectedCallback() { + document.removeEventListener('mci.chosen-market-size-changed', this._chosenMarketSizeChangedEventHandler); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + this.innerHTML = ` +
    + + +
    ${this.translation('markets-to')}
    + + +
    ${this._comparisonElementHTML()}
    +
    + `; + + [this._fromMarketsComparatorInput, this._toMarketsComparatorInput] = this.querySelectorAll("markets-comparator-input"); + this._comparisonElement = this.querySelector('[data-comparison-element]'); + } + + _comparisonElementHTML() { + if (!this._fromMarketSize || !this._toMarketSize) { + return '-'; + } + return `${this._fromMarketSize.toExponential(3)} / ${this._toMarketSize.toExponential(3)} = ${this._chosenMarketsComparedValue()}`; + } + + _chosenMarketsComparedValue() { + if (!this._fromMarketSize || !this._toMarketSize) { + return 0; + } + return Math.round(this._fromMarketSize * 1000 / this._toMarketSize) / 1000.0; + } + + _renderComparisonElementHTML() { + this._comparisonElement.innerHTML = this._comparisonElementHTML(); + } +} + +function assetsFromAttributes(element) { + const countAttribute = element.getAttribute("asset-items"); + if (!countAttribute) { + return []; + } + const assets = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`asset-${i}-name`); + const marketSize = element.getAttribute(`asset-${i}-market-size`); + if (name && marketSize) { + assets.push({ name, marketSize: parseInt(marketSize) }); + } + } + return assets; +} + +function currenciesFromAttributes(element) { + const countAttribute = element.getAttribute("currency-items"); + if (!countAttribute) { + return []; + } + const currencies = []; + for (let i = 0; i < parseInt(countAttribute); i++) { + const name = element.getAttribute(`currency-${i}-name`); + const marketSize = element.getAttribute(`currency-${i}-market-size`); + if (name && marketSize) { + currencies.push({ name, marketSize: parseInt(marketSize) }); + } + } + return currencies; +} + +class MarketsComparatorInput extends BaseHTMLElement { + + static observedAttributes = ["asset-items", "currency-items"]; + + /** @type {AssetOrCurrency[]} */ + _assets = []; + /** @type {AssetOrCurrency[]} */ + _currencies = []; + /** @type {AssetOrCurrency[]} */ + _assetOrCurrencyOptions = []; + + set assets(value) { + this._assets = value; + this._recalculateAssetOrCurrencyOptions(); + } + + set currencies(value) { + this._currencies = value; + this._recalculateAssetOrCurrencyOptions(); + } + + _assetOrCurrencyInput = null; + _curencyTurnoverInputMultiplier = 1; + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name == 'asset-items') { + this.assets = assetsFromAttributes(this); + } else if (name == 'currency-items') { + this.currencies = currenciesFromAttributes(this); + } + } + + _render() { + const optionsZIndex = this.getAttribute("options-z-index") ?? '99'; + this.innerHTML = ` + +
    + ${this._dropDownHeaderHTML()} +
    +
      + ${this._optionsHTML()} +
    +
    + `; + + this._setOptionsClickHandlers(); + + this.querySelector('[data-drop-down-header]') + .querySelector("input")?.addEventListener("input", e => { + this._curencyTurnoverInputMultiplier = e.target.value; + this._calculateChosenMarketSizeChange(); + }); + } + + _optionsHTML() { + return this._assetOrCurrencyOptions.map(o => + `
  • ${o.name}
  • `) + .join('\n'); + } + + _dropDownHeaderHTML() { + let marketSizeHTML; + if (this._isAsset()) { + marketSizeHTML = `${this.translation('market-size-input-label')} + + ${this.translation('days-turnover-input-label')} + `; + } else { + marketSizeHTML = ``; + } + return ` + ${this._assetOrCurrencyInput ?? this.translation('asset-or-currency-input-placeholder')} + ${marketSizeHTML} + `; + } + + _setOptionsClickHandlers() { + [...this.querySelectorAll("li")].forEach(o => { + o.onclick = () => { + this._assetOrCurrencyInput = o.getAttribute("data-option-id"); + this._calculateChosenMarketSizeChange(); + this._render(); + }; + }); + } + + _renderOptionsHTML() { + const optionsContainer = this.querySelector("ul"); + if (optionsContainer) { + optionsContainer.innerHTML = this._optionsHTML(); + this._setOptionsClickHandlers(); + } + } + + _isAsset() { + return this._assetOrCurrencyInput ? this._assets.find(a => a.name == this._assetOrCurrencyInput) : false; + } + + _isCurrency() { + return this._assetOrCurrencyInput ? this._currencies.find(c => c.name == this._assetOrCurrencyInput) : false; + } + + _recalculateAssetOrCurrencyOptions() { + const assetOrCurrencyOptions = []; + this._assets.forEach(a => assetOrCurrencyOptions.push(a)); + this._currencies.forEach(c => assetOrCurrencyOptions.push(c)); + this._assetOrCurrencyOptions = assetOrCurrencyOptions; + + this._renderOptionsHTML(); + this._calculateChosenMarketSizeChange(); + } + + _calculateChosenMarketSizeChange() { + if (!this._assetOrCurrencyInput) { + return; + } + + const assetInput = this._assets.find(a => a.name == this._assetOrCurrencyInput); + const currencyInput = this._currencies.find(c => c.name == this._assetOrCurrencyInput); + + const inputMarketSize = assetInput ? assetInput.marketSize : currencyInput.marketSize * this._curencyTurnoverInputMultiplier; + + this.dispatchEvent(new CustomEvent("mci.chosen-market-size-changed", { + bubbles: true, + detail: { name: this._assetOrCurrencyInput, marketSize: inputMarketSize } + })); + } +} + + +/** +* @typedef {Object} AssetOrCurrencyProjection +* @property {number} marketSize +* @property {number} growthRate +*/ + +class ProjectionsCalculator extends BaseHTMLElement { + + /** @type {?AssetOrCurrency} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrency} */ + _assetOrCurrency2 = null; + /** @type {?number} */ + _assetOrCurrency1ExpectedGrowthRate = null; + /** @type {?number} */ + _assetOrCurrency2ExpectedGrowthRate = null; + /** @type {?number} */ + _customProjectionYears = null; + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._renderProjectionsResultsHTML(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._renderProjectionsResultsHTML(); + } + + _assetOrCurrency1Header = null; + _assetOrCurrency1Input = null; + _assetOrCurrency2Header = null; + _assetOrCurrency2Input = null; + _customProjectionInput = null; + _projectionsResultsContainer = null; + _customProjectionContainer = null; + _customProjectionTextElement = null; + _customProjectionResultContainer = null; + + connectedCallback() { + this.innerHTML = ` +
    +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency1)}
    + +
    ${this._assetOrCurrencyHeaderText(this._assetOrCurrency2)}
    + +
    + ${this._projectionsResultsHTML()} +
    +
    + ${this._customProjectionHTML()} +
    +
    + `; + + const container = this.querySelector("div"); + + const divs = container.querySelectorAll("div"); + [this._assetOrCurrency1Header, this._assetOrCurrency2Header, this._projectionsResultsContainer] = divs; + this._customProjectionContainer = divs[divs.length - 1]; + + [this._assetOrCurrency1Input, this._assetOrCurrency2Input, + this._customProjectionInput] = container.querySelectorAll("input"); + + this._assetOrCurrency1Input.addEventListener("input", e => { + this._assetOrCurrency1ExpectedGrowthRate = parseInt(e.target.value); + this._renderProjectionsResultsHTML(); + }); + this._assetOrCurrency2Input.addEventListener("input", e => { + this._assetOrCurrency2ExpectedGrowthRate = parseInt(e.target.value); + this._renderProjectionsResultsHTML(); + }); + this._customProjectionInput.addEventListener("input", e => { + this._customProjectionYears = parseInt(e.target.value); + this._updateCustomProjectionText(); + this._updateCustomProjectionResult(); + }); + + this._customProjectionTextElement = this.querySelector('[data-custom-projection-text-element]'); + this._customProjectionResultContainer = this.querySelector('[data-custom-projection-result-container]'); + } + + _assetOrCurrencyHeaderText(assetOrCurrency) { + return `${assetOrCurrency ? assetOrCurrency.name : this.translation('asset-or-currency-placeholder')} ${this.translation('asset-or-currency-expected-annual-growth-rate')}:`; + } + + _renderProjectionsResultsHTML() { + if (this._projectionsResultsContainer && this._customProjectionContainer) { + this._projectionsResultsContainer.innerHTML = this._projectionsResultsHTML(); + this._updateCustomProjectionResult(); + } + } + + _projectionsResultsHTML() { + const inYearsText = (years) => `${this.translation('results-in-header')} ${years} ${this.translation(years == 1 ? 'year' : 'years')}`; + const currentYear = new Date().getFullYear(); + return [1, 5, 10].map(y => ` +
    ${inYearsText(y)} (${currentYear + y}):
    + ${this._projectionsResultHTML(y)}`) + .join('\n'); + } + + _projectionsResultHTML(years) { + const ac1 = this._assetOrCurrency1WithExpectedGrowthRate(); + const ac2 = this._assetOrCurrency2WithExpectedGrowthRate(); + if (years != null && ac1 != null && ac2 != null) { + return ` + + `; + } + return '
    -
    '; + } + + _customProjectionHTML() { + return ` + ${this.translation('results-in-header')} + ${this._customProjectionYearText()}: +
    ${this._projectionsResultHTML(this._customProjectionYears)}
    + `; + } + + _customProjectionYearText() { + const currentYear = new Date().getFullYear(); + return '(' + (this._customProjectionYears == null || Number.isNaN(this._customProjectionYears) ? + `${currentYear} + N` : (currentYear + this._customProjectionYears)) + ')'; + } + + _updateCustomProjectionText() { + this._customProjectionTextElement.textContent = this._customProjectionYearText(); + } + + _updateCustomProjectionResult() { + this._customProjectionContainer.innerHTML = this._projectionsResultHTML(this._customProjectionYears); + } + + _assetOrCurrency1WithExpectedGrowthRate() { + if (this._assetOrCurrency1 && this._assetOrCurrency1ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency1.marketSize, growthRate: this._assetOrCurrency1ExpectedGrowthRate }; + } + return null; + } + + _assetOrCurrency2WithExpectedGrowthRate() { + if (this._assetOrCurrency2 && this._assetOrCurrency2ExpectedGrowthRate != null) { + return { marketSize: this._assetOrCurrency2.marketSize, growthRate: this._assetOrCurrency2ExpectedGrowthRate }; + } + return null; + } +} + +class ProjectionsResult extends HTMLElement { + + static observedAttributes = [ + "years", + "asset-or-currency-1-market-size", "asset-or-currency-1-growth-rate", + "asset-or-currency-2-market-size", "asset-or-currency-2-growth-rate" + ]; + + /** @type {number} */ + _years = 1; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency1 = null; + /** @type {?AssetOrCurrencyProjection} */ + _assetOrCurrency2 = null; + + set years(value) { + this._years = value; + this._render(); + } + + set assetOrCurrency1(value) { + this._assetOrCurrency1 = value; + this._render(); + } + + set assetOrCurrency2(value) { + this._assetOrCurrency2 = value; + this._render(); + } + + connectedCallback() { + this._render(); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name.includes("asset-or-currency-1")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-1"); + if (assetOrCurrency) { + this.assetOrCurrency1 = assetOrCurrency; + } + } else if (name.includes("asset-or-currency-2")) { + const assetOrCurrency = this._assetOrCurrencyFromAttributes("asset-or-currency-2"); + if (assetOrCurrency) { + this.assetOrCurrency2 = assetOrCurrency; + } + } else if (name == 'years') { + this.years = parseInt(newValue); + } + } + + _assetOrCurrencyFromAttributes(prefix) { + const marketSize = this.getAttribute(`${prefix}-market-size`); + const growthRate = this.getAttribute(`${prefix}-growth-rate`); + if (marketSize && growthRate) { + return { marketSize: parseInt(marketSize), growthRate: parseInt(growthRate) }; + } + return null; + } + + _render() { + const nominator = this._exponentialNumberString(this._projectionNumerator()); + const denominator = this._exponentialNumberString(this._projectionDenominator()); + this.innerHTML = ` +
    ${nominator} / ${denominator} = ${this._projection()} +
    + `; + } + + _exponentialNumberString(n) { + return n?.toExponential(3) ?? ''; + } + + _marketSizeChangedByRate(marketSize, growthRate, decrease = false) { + let changedMarketSize; + if (decrease) { + changedMarketSize = marketSize - (marketSize * growthRate / 100.0); + } else { + changedMarketSize = marketSize + (marketSize * growthRate / 100.0); + } + if (changedMarketSize <= 0 || Number.isNaN(changedMarketSize)) { + return null; + } + return changedMarketSize; + } + + _marketSizeChangedByRateInGivenYears(marketSize, growthRate, years) { + const negativeYears = years < 0; + let increasedMarketSize = marketSize; + for (let i = 0; i < Math.abs(years); i++) { + increasedMarketSize = this._marketSizeChangedByRate(increasedMarketSize, growthRate, negativeYears); + if (!increasedMarketSize) { + return null; + } + } + return increasedMarketSize; + } + + _projectionNumerator() { + if (this._assetOrCurrency1) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency1.marketSize, + this._assetOrCurrency1.growthRate, + this._years); + } + return null; + } + + _projectionDenominator() { + if (this._assetOrCurrency2) { + return this._marketSizeChangedByRateInGivenYears( + this._assetOrCurrency2.marketSize, + this._assetOrCurrency2.growthRate, + this._years); + } + return null; + } + + _projection() { + const numerator = this._projectionNumerator(); + const denominator = this._projectionDenominator(); + if (numerator && denominator) { + return Math.round(numerator * 1000 / denominator) / 1000.0; + } + return ''; + } +} + + +class MarketsProjections extends BaseHTMLElement { + + _marketsComparatorComponent = null; + _projectionsCalculatorComponent = null; + + /** @type {?AssetOrCurrency} */ + _fromAssetOrCurrency = null; + /** @type {?AssetOrCurrency} */ + _toAssetOrCurrency = null; + + /** @type {AssetOrCurrency[]} */ + set assets(value) { + if (this._marketsComparatorComponent) { + this._marketsComparatorComponent.assets = value; + } + } + + /** @type {AssetOrCurrency[]} */ + set currencies(value) { + if (this._marketsComparatorComponent) { + this._marketsComparatorComponent.currencies = value; + } + } + + connectedCallback() { + this.innerHTML = ` +
    +

    ${this.translation('projections-header')}

    + + + + +
    + `; + + this._marketsComparatorComponent = this.querySelector("markets-comparator"); + this._projectionsCalculatorComponent = this.querySelector("projections-calculator"); + + const fromMarketSizeChangedEventHandler = e => { + this._fromAssetOrCurrency = e.detail; + this._projectionsCalculatorComponent.assetOrCurrency1 = this._fromAssetOrCurrency; + }; + const toMarketSizeChangedEventHandler = e => { + this._toAssetOrCurrency = e.detail; + this._projectionsCalculatorComponent.assetOrCurrency2 = this._toAssetOrCurrency; + }; + + this.addEventListener('mc.from-market-size-changed', fromMarketSizeChangedEventHandler); + this.addEventListener('mc.to-market-size-changed', toMarketSizeChangedEventHandler); + } +} + + +export function registerComponents() { + customElements.define('tabs-container', TabsContainer); + customElements.define('tab-header', TabHeader); + customElements.define("drop-down-container", DropDownContainer); + customElements.define("asset-element", AssetElement); + customElements.define("currency-element", CurrencyElement); + customElements.define('assets-and-currencies', AssetsAndCurrencies); + customElements.define("markets-header", MarketsHeader); + customElements.define("markets-comparator-input", MarketsComparatorInput); + customElements.define('markets-comparator', MarketsComparator); + customElements.define("projections-result", ProjectionsResult); + customElements.define("projections-calculator", ProjectionsCalculator); + customElements.define('markets-projections', MarketsProjections); +} \ No newline at end of file diff --git a/web-components-reuse/vue-app/src/data/api.ts b/web-components-reuse/vue-app/src/data/api.ts index 338a905f..be94f283 100644 --- a/web-components-reuse/vue-app/src/data/api.ts +++ b/web-components-reuse/vue-app/src/data/api.ts @@ -1,6 +1,8 @@ import type { CurrencyCode } from "./currency-code"; import { MockedApi } from "./mocked-api"; +// TODO: simplify types + export interface Asset { id: string; name: string; diff --git a/web-components-reuse/vue-app/src/main.ts b/web-components-reuse/vue-app/src/main.ts index 58cb87d2..4096d7a1 100644 --- a/web-components-reuse/vue-app/src/main.ts +++ b/web-components-reuse/vue-app/src/main.ts @@ -6,7 +6,7 @@ import { createRouter, createWebHistory } from 'vue-router'; import Home from './components/Home.vue'; // @ts-ignore -import { registerComponents } from './components/lib/registry.js'; +import { registerComponents } from './components/web-components.js'; registerComponents(); @@ -24,7 +24,7 @@ const i18n = createI18n({ messages: { en: { 'markets-header': { - 'markets-in': "Market in", + 'markets-in': "Markets in", 'live-updates': "Live updates:", 'live-updates-on': "ON", 'live-updates-off': "OFF" From f19a89f516ef9871c8df494fc797a9e2c683a176 Mon Sep 17 00:00:00 2001 From: Igor Date: Sat, 4 Oct 2025 20:41:59 +0200 Subject: [PATCH 07/13] Refactor example types --- web-components-reuse/components/package.py | 2 +- .../components/src/markets-header.js | 6 +- .../vue-app/src/components/Home.vue | 32 +++---- .../vue-app/src/components/web-components.js | 8 +- web-components-reuse/vue-app/src/data/api.ts | 7 +- .../vue-app/src/data/codes.ts | 26 ++++++ .../vue-app/src/data/currency-code.ts | 52 ------------ .../vue-app/src/data/mocked-api.ts | 85 +++++++++---------- web-components-reuse/vue-app/src/main.ts | 16 ++++ 9 files changed, 108 insertions(+), 126 deletions(-) create mode 100644 web-components-reuse/vue-app/src/data/codes.ts delete mode 100644 web-components-reuse/vue-app/src/data/currency-code.ts diff --git a/web-components-reuse/components/package.py b/web-components-reuse/components/package.py index 6c9baebb..fc2ca20f 100644 --- a/web-components-reuse/components/package.py +++ b/web-components-reuse/components/package.py @@ -32,7 +32,7 @@ def git_metadata(): return f'{branch}:{commit_hash}' -metadata = f'Generated from {git_metadata()}' +metadata = f'Generated by components/{path.basename(__file__)} from {git_metadata()}' for o in config['outputs']: with open(o, "w") as of: diff --git a/web-components-reuse/components/src/markets-header.js b/web-components-reuse/components/src/markets-header.js index b78f6e93..101648db 100644 --- a/web-components-reuse/components/src/markets-header.js +++ b/web-components-reuse/components/src/markets-header.js @@ -12,7 +12,7 @@ class MarketsHeader extends BaseHTMLElement { set denomination(value) { this._denomination = value; if (this._denominationElement) { - this._denominationElement = this._denomination; + this._denominationElement.textContent = this._denomination; } } @@ -40,7 +40,7 @@ class MarketsHeader extends BaseHTMLElement { this._liveUpdatesEnabledElement.onclick = () => { this._liveUpdatesEnabled = !this._liveUpdatesEnabled; this._liveUpdatesEnabledElement.textContent = this._liveUpdatesElementText(); - document.dispatchEvent(new CustomEvent('mh:live-updates-toggled', { detail: this._liveUpdatesEnabled })); + this.dispatchEvent(new CustomEvent('mh.live-updates-toggled', { bubbles: true, detail: this._liveUpdatesEnabled })); }; this._denominationElement = this.querySelector("drop-down-container > span"); @@ -63,7 +63,7 @@ class MarketsHeader extends BaseHTMLElement { o.onclick = () => { this._denomination = o.getAttribute("data-option-id"); this._denominationElement.textContent = this._denomination; - this.dispatchEvent(new CustomEvent('mh:denomination-changed', { bubbles: true, detail: this._denomination })); + this.dispatchEvent(new CustomEvent('mh.denomination-changed', { bubbles: true, detail: this._denomination })); }; }); } diff --git a/web-components-reuse/vue-app/src/components/Home.vue b/web-components-reuse/vue-app/src/components/Home.vue index a84f3a17..3bdc6345 100644 --- a/web-components-reuse/vue-app/src/components/Home.vue +++ b/web-components-reuse/vue-app/src/components/Home.vue @@ -2,13 +2,13 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { type Asset, type Currency, type ExchangeRate, api } from '../data/api'; -import { USD, type CurrencyCode, currencyCodeById } from '../data/currency-code'; +import { CurrencyCode} from '../data/codes'; import { useUpdater } from '../data/updater'; const { t } = useI18n(); const liveUpdatesEnabled = ref(true); -const denomination = ref(USD); +const denomination = ref(CurrencyCode.USD); const denominationExchangeRate = ref(1); const denominationExchangeRates = ref([]); const assets = ref([]); @@ -50,7 +50,7 @@ const onDenominationChanged = () => { const updateDenominationExchangeRates = async () => { const exchangeRates = await api.exchangeRates(); - const fromDollarToDenominationExchangeRate = exchangeRates.find(er => er.to.id == denomination.value.id)?.value ?? 1; + const fromDollarToDenominationExchangeRate = exchangeRates.find(er => er.to == denomination.value)?.value ?? 1; const fromDenominationToDollarExchangeRate = 1 / fromDollarToDenominationExchangeRate; denominationExchangeRates.value = exchangeRates.map(er => ({ from: denomination.value, @@ -60,10 +60,13 @@ const updateDenominationExchangeRates = async () => { }; const assetInputOptions = computed<{ name: string, marketSize: number }[]>(() => - assets.value.map(a => ({ name: a.name, marketSize: a.marketSize }))); + assets.value.map(a => ({ name: assetName(a), marketSize: a.marketSize }))); const currencyInputOptions = computed<{ name: string, marketSize: number }[]>(() => - currencies.value.map(c => ({ name: c.code.name, marketSize: c.marketSize }))); + currencies.value.map(c => ({ name: currencyName(c), marketSize: c.marketSize }))); + +const assetName = (a: Asset) => t('asset-code.' + a.code); +const currencyName = (c: Currency) => t('currency-code.' + c.code); onDenominationChanged(); @@ -73,31 +76,30 @@ const liveUpdatesToggledEventHandler = (e: Event) => { }; const denominationChangedEventHandler = (e: Event) => { - const denominationId = (e as CustomEvent).detail as string; - denomination.value = currencyCodeById(denominationId); + denomination.value = (e as CustomEvent).detail as CurrencyCode; onDenominationChanged(); }; onMounted(() => { - document.addEventListener('mh:live-updates-toggled', liveUpdatesToggledEventHandler); - document.addEventListener('mh:denomination-changed', denominationChangedEventHandler); + document.addEventListener('mh.live-updates-toggled', liveUpdatesToggledEventHandler); + document.addEventListener('mh.denomination-changed', denominationChangedEventHandler); }); onUnmounted(() => { - document.removeEventListener('mh:live-updates-toggled', liveUpdatesToggledEventHandler); - document.removeEventListener('mh:denomination-changed', denominationChangedEventHandler); + document.removeEventListener('mh.live-updates-toggled', liveUpdatesToggledEventHandler); + document.removeEventListener('mh.denomination-changed', denominationChangedEventHandler); });