diff --git a/Cargo.lock b/Cargo.lock index 6cbdc7837..2496fab23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -639,9 +639,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.16" +version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ "jobserver", "libc", @@ -1789,9 +1789,9 @@ checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" [[package]] name = "oxc" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e398ac9650c77d6a43e7f5ed5315c3cae33fd1f84666acd0a55719c8da1555b3" +checksum = "d6f2d8bbd880aaaf838456ce101c59d926a762b6a891ef91402794e9dc8d2c2c" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1832,9 +1832,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36dd2ba553043fd1cf1f92fb4d685a9a58afcc4f2692e85ebf84e242e6492909" +checksum = "fe07aea78e1e1a860d92cfe1b712f81ba10960dee847c6231fa4e9b0665ec5ff" dependencies = [ "allocator-api2", "bumpalo", @@ -1846,9 +1846,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0607fb00e5f2413b5a99b36eff638b7db57e69149e98ac693d2aaa500d1d26" +checksum = "062ec80f1ed9471bc05f57bd481bd4921285373b57018f3028aed49cb3ac353f" dependencies = [ "bitflags", "cow-utils", @@ -1863,9 +1863,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf0d4b6faf22509c64f78d4a0a0bb760f6ba34fe7acdcb421b57fdc32482867e" +checksum = "41e79130c01eaddff0274d504404f80d88835ed70fcc1e91f9c9fd42fd718202" dependencies = [ "proc-macro2", "quote", @@ -1874,9 +1874,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421e72f280ed323f63ad7ca5e4700577ca51f7946fdc9868baeb7e23eeb1c6b1" +checksum = "1c0476717291544c614de9ffc1c34c29b06025008c2e604505e67248234725ca" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1886,9 +1886,9 @@ dependencies = [ [[package]] name = "oxc_cfg" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c213a97298278d4f9a7e3a4e6bce0c5eba416aed5b291a6296bbb8c26ca1e65" +checksum = "243535bf553c8d399f20a392eabb6eff5990818eac8519a930a1c345497a8ea7" dependencies = [ "bitflags", "itertools", @@ -1901,9 +1901,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a61b0ca6f9d8e9fb6a5a5390ecae8bbd98283cfb38d24ce77979b77ab28fc65" +checksum = "4ebcbed8d477c4b9142c895a762be0afd16bd0838f64237e2c006fd9f8ec7e1a" dependencies = [ "bitflags", "cow-utils", @@ -1922,15 +1922,15 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c43341de573281bc1883b4cdb36dd9c8c11e56a7d6fda0b8335471add52d2" +checksum = "e4c79e0d91ca11d4add13d94f802096564babb2c609956a8e19eb6f83b7f0fb1" [[package]] name = "oxc_diagnostics" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f5ce8bc7ebc0fa2c2bd94d526a92636e8622f879a9dd9a41b6c77c12a2b2408" +checksum = "b127d339db14984b22bf4255a2583c1be2cd709b1b14f64e3ce4cb5fe87c699d" dependencies = [ "cow-utils", "oxc-miette", @@ -1938,9 +1938,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce89c6c2764fa6ad1f929a91e09614943fe7a25df1d288d38acae0302581b8f" +checksum = "522b7c4d6db500536be627e1d3952cf26705328e77a4d819a2543c2392b702eb" dependencies = [ "cow-utils", "num-bigint", @@ -1952,9 +1952,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a4f605c6f6460d2cb659bb1c2273244ebf8c07bae4155274fa2461d7e0ec35" +checksum = "7190d1db8c149324345b14588f24a318712498fde1741513c3a129731ef6b4f9" [[package]] name = "oxc_index" @@ -1964,9 +1964,9 @@ checksum = "2fa07b0cfa997730afed43705766ef27792873fdf5215b1391949fec678d2392" [[package]] name = "oxc_mangler" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73d8be639f09e7c62e4503a5cbe3a802d78265c490b09ebaa1fb905d5b9d8bb0" +checksum = "cddde40dd8422c56c07eecd370d5c221626afb5cb5966824c1884a8b929305cb" dependencies = [ "fixedbitset", "itertools", @@ -1981,9 +1981,9 @@ dependencies = [ [[package]] name = "oxc_minifier" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea781d79be82fd4e1cd94fe6a6da4517edf075cc604d52f9785a0153157736d8" +checksum = "513dd618e9016cb00cc52e547bcbdb160a0379025206f8cce14287c1c624b57a" dependencies = [ "cow-utils", "oxc_allocator", @@ -2003,9 +2003,9 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b633fe51f19c4da6f3cd6fd0885e3a9e12f19317487a477fea0f73892713083" +checksum = "afda2c4a47704ff4c68990248be9f693f606626cff1c6d1760034638b54c5413" dependencies = [ "bitflags", "cow-utils", @@ -2026,9 +2026,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35b1a9babb738e4d99cfc0ef8ad242806d261de028b400a3205afdf7a9a1c03" +checksum = "a4354d5b5f48d53cc0d4d000425ec062b12e9b3fbcf395e765064c8eab113921" dependencies = [ "oxc_allocator", "oxc_ast_macros", @@ -2042,9 +2042,9 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78fcfacfa8a2bb020326c37011b86f423c41e776516c197e6c162ac85b6a1e7a" +checksum = "e1346102f3550e6b0417fe84f8fb397f976c351e21bcb98b75ff018cc3877bd3" dependencies = [ "itertools", "oxc_allocator", @@ -2078,9 +2078,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2940d9a7446ddbe554bf0aa3cd111e6bf7c2dd29460da6673cde9b1c7be77f" +checksum = "77b073cb1349f33e04d821e4fd1e51c860a010d74fea0b4660504cb05a87968a" dependencies = [ "compact_str", "oxc-miette", @@ -2091,9 +2091,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9d42c1d620a1da919ec6b56c4476054d4d2c71423c08effbc3a0519c516b61" +checksum = "488a2404fca5b741255b8b875e8f6515f5b08df6046a0767b2368d6182e055cb" dependencies = [ "bitflags", "cow-utils", @@ -2112,9 +2112,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.61.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f389b5904bc2294fe53ee6b0f6b3b15e395b71079168d8e8204626c4aede3c32" +checksum = "02102521f55df8330e9393d425e746e21d9badc5a78650769e645db40102f3c0" dependencies = [ "compact_str", "itoa", @@ -3473,18 +3473,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.3" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ "cc", "pkg-config", diff --git a/crates/brk_indexer/src/lib.rs b/crates/brk_indexer/src/lib.rs index 3eb318877..c138d6d09 100644 --- a/crates/brk_indexer/src/lib.rs +++ b/crates/brk_indexer/src/lib.rs @@ -31,7 +31,7 @@ pub use stores::*; pub use vecs::*; const SNAPSHOT_BLOCK_RANGE: usize = 1000; -const COLLISIONS_CHECKED_UP_TO: u32 = 870_000; +const COLLISIONS_CHECKED_UP_TO: u32 = 888_000; #[derive(Clone)] pub struct Indexer { diff --git a/crates/brk_indexer/src/vecs/base.rs b/crates/brk_indexer/src/vecs/base.rs index 0b7f25e31..a05f4ed21 100644 --- a/crates/brk_indexer/src/vecs/base.rs +++ b/crates/brk_indexer/src/vecs/base.rs @@ -162,17 +162,6 @@ where } } -// impl Deref for StorableVec { -// type Target = brk_vec::StorableVec; -// fn deref(&self) -> &Self::Target { -// &self.vec -// } -// } -// impl DerefMut for StorableVec { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.vec -// } -// } impl Clone for StorableVec where I: StoredIndex, diff --git a/crates/brk_server/Cargo.toml b/crates/brk_server/Cargo.toml index 45220f590..d481c938f 100644 --- a/crates/brk_server/Cargo.toml +++ b/crates/brk_server/Cargo.toml @@ -21,7 +21,7 @@ color-eyre = { workspace = true } jiff = { workspace = true } log = { workspace = true } minreq = { workspace = true } -oxc = { version = "0.61.0", features = ["codegen", "minifier"] } +oxc = { version = "0.61.1", features = ["codegen", "minifier"] } serde = { workspace = true } tokio = { version = "1.44.1", features = ["full"] } tower-http = { version = "0.6.2", features = ["compression-full"] } diff --git a/websites/kibo.money/index.html b/websites/kibo.money/index.html index a48b547c3..93166e466 100644 --- a/websites/kibo.money/index.html +++ b/websites/kibo.money/index.html @@ -1518,7 +1518,6 @@ " >希望 - 希望.お金 Bitcoin is diff --git a/websites/kibo.money/packages/lightweight-charts/wrapper.js b/websites/kibo.money/packages/lightweight-charts/wrapper.js index 4a04d2149..37e2fc68e 100644 --- a/websites/kibo.money/packages/lightweight-charts/wrapper.js +++ b/websites/kibo.money/packages/lightweight-charts/wrapper.js @@ -4,19 +4,19 @@ * @import { DeepPartial, ChartOptions, IChartApi, IHorzScaleBehavior, WhitespaceData, SingleValueData, ISeriesApi, Time, LogicalRange, SeriesType, BaselineStyleOptions, SeriesOptionsCommon, createChart as CreateClassicChart, createChartEx as CreateCustomChart } from "./v4.2.2/types" */ -const ids = { - from: "from", - to: "to", - chartRange: "chart-range", - /** - * @param {TimeScale} scale - */ - visibleTimeRange(scale) { - return `${ids.chartRange}-${scale}`; - }, -}; - export default import("./v4.2.2/script.js").then((lightweightCharts) => { + const ids = { + from: "from", + to: "to", + chartRange: "chart-range", + /** + * @param {TimeScale} scale + */ + visibleTimeRange(scale) { + return `${ids.chartRange}-${scale}`; + }, + }; + const createClassicChart = /** @type {CreateClassicChart} */ ( lightweightCharts.createChart ); @@ -357,6 +357,7 @@ export default import("./v4.2.2/script.js").then((lightweightCharts) => { * @param {TimeScale} param0.scale * @param {"static" | "moveable"} param0.kind * @param {Utilities} param0.utils + * @param {Constants} param0.consts * @param {Owner | null} [param0.owner] * @param {CreatePaneParameters[]} [param0.config] */ @@ -368,6 +369,7 @@ export default import("./v4.2.2/script.js").then((lightweightCharts) => { kind, scale, config, + consts, utils, owner: _owner, }) { diff --git a/websites/kibo.money/scripts/chart.js b/websites/kibo.money/scripts/chart.js index a777c694e..6da5a0dc4 100644 --- a/websites/kibo.money/scripts/chart.js +++ b/websites/kibo.money/scripts/chart.js @@ -8,6 +8,7 @@ * @param {Signals} args.signals * @param {Utilities} args.utils * @param {Datasets} args.datasets + * @param {Constants} args.consts * @param {WebSockets} args.webSockets * @param {Elements} args.elements */ @@ -19,6 +20,7 @@ export function init({ selected, signals, utils, + consts, webSockets, }) { console.log("init chart state"); @@ -43,6 +45,7 @@ export function init({ id: "chart", scale: scale(), kind: "moveable", + consts, utils, }); diff --git a/websites/kibo.money/scripts/live-price.js b/websites/kibo.money/scripts/live-price.js index e1c7f148e..959c05744 100644 --- a/websites/kibo.money/scripts/live-price.js +++ b/websites/kibo.money/scripts/live-price.js @@ -7,7 +7,7 @@ /** * @param {Object} args * @param {Colors} args.colors - * @param {Consts} args.consts + * @param {Constants} args.consts * @param {LightweightCharts} args.lightweightCharts * @param {Signals} args.signals * @param {Utilities} args.utils diff --git a/websites/kibo.money/scripts/main.js b/websites/kibo.money/scripts/main.js index 20dc050d4..41df8336e 100644 --- a/websites/kibo.money/scripts/main.js +++ b/websites/kibo.money/scripts/main.js @@ -68,34 +68,35 @@ function initPackages() { ufuzzy: importPackage("ufuzzy"), }; } -const packages = initPackages(); /** - * @typedef {Awaited>} LightweightCharts + * @typedef {ReturnType} Packages + * @typedef {Awaited>} LightweightCharts * @typedef {ReturnType} Chart */ -const options = import("./options.js"); - -const utils = { +function createUtils() { /** * @param {string} serialized * @returns {boolean} */ - isSerializedBooleanTrue(serialized) { + function isSerializedBooleanTrue(serialized) { return serialized === "true" || serialized === "1"; - }, + } + /** * @param {number} ms */ - sleep(ms) { + function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); - }, - yield() { - return this.sleep(0); - }, - array: { + } + + function next() { + return sleep(0); + } + + const array = { /** * @param {number} start * @param {number} end @@ -108,8 +109,9 @@ const utils = { } return range; }, - }, - dom: { + }; + + const dom = { /** * @param {string} id * @returns {HTMLElement} @@ -121,8 +123,9 @@ const utils = { }, /** * @param {string} name + * @param {Elements} elements */ - queryOrCreateMetaElement(name) { + queryOrCreateMetaElement(name, elements) { let meta = /** @type {HTMLMetaElement | null} */ ( window.document.querySelector(`meta[name="${name}"]`) ); @@ -289,9 +292,10 @@ const utils = { }, /** * @param {string} url + * @param {Elements} elements * @param {boolean} [targetBlank] */ - open(url, targetBlank) { + open(url, elements, targetBlank) { console.log(`open: ${url}`); const a = window.document.createElement("a"); elements.body.append(a); @@ -322,7 +326,8 @@ const utils = { link.type = "text/css"; link.rel = "stylesheet"; link.media = "screen,print"; - elements.head.appendChild(link); + const head = window.document.getElementsByTagName("head")[0]; + head.appendChild(link); return link; }, /** @@ -362,7 +367,7 @@ const utils = { choices.forEach((choice) => { const inputValue = choice.toLowerCase(); - const { label } = utils.dom.createLabeledInput({ + const { label } = this.createLabeledInput({ inputId: `${id}-${choice.toLowerCase()}`, inputName: id, inputValue, @@ -476,7 +481,7 @@ const utils = { const min = "2011-01-01"; const minDate = new Date(min); const maxDate = new Date(); - const max = utils.date.toString(maxDate); + const max = date.toString(maxDate); input.min = min; input.max = max; @@ -484,8 +489,8 @@ const utils = { signals.createEffect( () => { - const date = signal(); - return date ? utils.date.toString(date) : ""; + const dateSignal = signal(); + return dateSignal ? date.toString(dateSignal) : ""; }, (value) => { if (stateValue !== value) { @@ -663,8 +668,9 @@ const utils = { div.classList.add(`shadow-${position}`); return div; }, - }, - url: { + }; + + const url = { chartParamsWhitelist: ["from", "to"], /** * @param {string} pathname @@ -736,7 +742,7 @@ const utils = { const parameter = this.readParam(key); if (parameter) { - return utils.isSerializedBooleanTrue(parameter); + return isSerializedBooleanTrue(parameter); } return null; @@ -767,20 +773,23 @@ const utils = { pathnameToSelectedId() { return window.document.location.pathname.substring(1); }, - }, - locale: { - /** - * @param {number} value - * @param {number} [digits] - * @param {Intl.NumberFormatOptions} [options] - */ - numberToUSFormat(value, digits, options) { - return value.toLocaleString("en-us", { - ...options, - minimumFractionDigits: digits, - maximumFractionDigits: digits, - }); - }, + }; + + /** + * @param {number} value + * @param {number} [digits] + * @param {Intl.NumberFormatOptions} [options] + */ + function numberToUSFormat(value, digits, options) { + return value.toLocaleString("en-us", { + ...options, + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); + } + + const locale = { + numberToUSFormat, /** @param {number} value */ numberToShortUSFormat(value) { const absoluteValue = Math.abs(value); @@ -788,15 +797,15 @@ const utils = { if (isNaN(value)) { return ""; } else if (absoluteValue < 10) { - return utils.locale.numberToUSFormat(value, 3); + return numberToUSFormat(value, 3); } else if (absoluteValue < 100) { - return utils.locale.numberToUSFormat(value, 2); + return numberToUSFormat(value, 2); } else if (absoluteValue < 1_000) { - return utils.locale.numberToUSFormat(value, 1); + return numberToUSFormat(value, 1); } else if (absoluteValue < 100_000) { - return utils.locale.numberToUSFormat(value, 0); + return numberToUSFormat(value, 0); } else if (absoluteValue < 1_000_000) { - return `${utils.locale.numberToUSFormat(value / 1_000, 1)}K`; + return `${numberToUSFormat(value / 1_000, 1)}K`; } else if (absoluteValue >= 9_000_000_000_000_000) { return "Inf."; } @@ -810,24 +819,25 @@ const utils = { const modulused = log % 3; if (modulused === 0) { - return `${utils.locale.numberToUSFormat( + return `${numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 3, )}${letter}`; } else if (modulused === 1) { - return `${utils.locale.numberToUSFormat( + return `${numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 2, )}${letter}`; } else { - return `${utils.locale.numberToUSFormat( + return `${numberToUSFormat( value / (1_000_000 * 1_000 ** letterIndex), 1, )}${letter}`; } }, - }, - storage: { + }; + + const storage = { /** * @param {string} key */ @@ -844,7 +854,7 @@ const utils = { readBool(key) { const saved = this.read(key); if (saved) { - return utils.isSerializedBooleanTrue(saved); + return isSerializedBooleanTrue(saved); } return null; }, @@ -869,8 +879,9 @@ const utils = { remove(key) { this.write(key, undefined); }, - }, - serde: { + }; + + const serde = { number: { /** * @param {number} v @@ -890,7 +901,7 @@ const utils = { * @param {Date} v */ serialize(v) { - return utils.date.toString(v); + return date.toString(v); }, /** * @param {string} v @@ -919,8 +930,9 @@ const utils = { } }, }, - }, - formatters: { + }; + + const formatters = { dollars: new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", @@ -932,8 +944,9 @@ const utils = { minimumFractionDigits: 2, maximumFractionDigits: 2, }), - }, - date: { + }; + + const date = { ONE_DAY_IN_MS: 1000 * 60 * 60 * 24, todayUTC() { const today = new Date(); @@ -991,8 +1004,9 @@ const utils = { differenceBetween(date1, date2) { return Math.abs(date1.valueOf() - date2.valueOf()) / this.ONE_DAY_IN_MS; }, - }, - color: { + }; + + const color = { /** * * @param {readonly [number, number, number, number, number, number, number, number, number]} A @@ -1086,14 +1100,15 @@ const utils = { }); return `#${r}${g}${b}`; }, - }, + }; + /** * * @template {(...args: any[]) => any} F * @param {F} callback * @param {number} [wait=250] */ - debounce(callback, wait = 250) { + function debounce(callback, wait = 250) { /** @type {number | undefined} */ let timeoutId; /** @type {Parameters} */ @@ -1110,45 +1125,69 @@ const utils = { }, wait); } }; - }, + } + /** * @param {VoidFunction} callback * @param {number} [timeout = 1] */ - runWhenIdle(callback, timeout = 1) { + function runWhenIdle(callback, timeout = 1) { if ("requestIdleCallback" in window) { requestIdleCallback(callback); } else { setTimeout(callback, timeout); } - }, + } + /** * @param {Date} oldest * @param {Date} youngest * @returns {number} */ - getNumberOfDaysBetweenTwoDates(oldest, youngest) { + function getNumberOfDaysBetweenTwoDates(oldest, youngest) { + const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24; return Math.round( - Math.abs((youngest.getTime() - oldest.getTime()) / consts.ONE_DAY_IN_MS), + Math.abs((youngest.getTime() - oldest.getTime()) / ONE_DAY_IN_MS), ); - }, + } + + /** + * @param {string} s + */ + function stringToId(s) { + return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); + } + /** * @param {TimeScale} scale * @param {number} id */ - chunkIdToIndex(scale, id) { - return scale === "date" - ? id - 2009 - : Math.floor(id / consts.HEIGHT_CHUNK_SIZE); - }, - /** - * @param {string} s - */ - stringToId(s) { - return s.replace(/\W/g, " ").trim().replace(/ +/g, "-").toLowerCase(); - }, -}; -/** @typedef {typeof utils} Utilities */ + function chunkIdToIndex(scale, id) { + const HEIGHT_CHUNK_SIZE = 10_000; + return scale === "date" ? id - 2009 : Math.floor(id / HEIGHT_CHUNK_SIZE); + } + + return { + isSerializedBooleanTrue, + sleep, + next, + array, + dom, + url, + locale, + storage, + serde, + formatters, + date, + color, + debounce, + runWhenIdle, + getNumberOfDaysBetweenTwoDates, + chunkIdToIndex, + stringToId, + }; +} +/** @typedef {ReturnType} Utilities */ function initEnv() { const standalone = @@ -1175,8 +1214,7 @@ function initEnv() { localhost: window.location.hostname === "localhost", }; } -const env = initEnv(); -/** @typedef {typeof env} Env */ +/** @typedef {ReturnType} Env */ function createConstants() { const ONE_SECOND_IN_MS = 1_000; @@ -1207,152 +1245,62 @@ function createConstants() { MEDIUM_WIDTH, }; } -const consts = createConstants(); -/** @typedef {typeof consts} Consts */ +/** @typedef {ReturnType} Constants */ -const ids = /** @type {const} */ ({ - selectedId: `selected-id`, - asideSelectorLabel: `aside-selector-label`, - checkedFrameSelectorLabel: "checked-frame-selector-label", -}); -/** @typedef {typeof ids} Ids */ - -const elements = { - head: window.document.getElementsByTagName("head")[0], - body: window.document.body, - main: utils.dom.getElementById("main"), - aside: utils.dom.getElementById("aside"), - asideLabel: utils.dom.getElementById(ids.asideSelectorLabel), - navLabel: utils.dom.getElementById(`nav-selector-label`), - searchLabel: utils.dom.getElementById(`search-selector-label`), - search: utils.dom.getElementById("search"), - nav: utils.dom.getElementById("nav"), - navHeader: utils.dom.getElementById("nav-header"), - searchInput: /** @type {HTMLInputElement} */ ( - utils.dom.getElementById("search-input") - ), - searchSmall: utils.dom.getElementById("search-small"), - searchResults: utils.dom.getElementById("search-results"), - selectors: utils.dom.getElementById("frame-selectors"), - style: getComputedStyle(window.document.documentElement), - charts: utils.dom.getElementById("charts"), - simulation: utils.dom.getElementById("simulation"), - livePrice: utils.dom.getElementById("live-price"), - moscowTime: utils.dom.getElementById("moscow-time"), -}; -/** @typedef {typeof elements} Elements */ - -const urlSelected = utils.url.pathnameToSelectedId(); - -function initFrameSelectors() { - const children = Array.from(elements.selectors.children); - - /** @type {HTMLElement | undefined} */ - let focusedFrame = undefined; - - for (let i = 0; i < children.length; i++) { - const element = children[i]; - - switch (element.tagName) { - case "LABEL": { - element.addEventListener("click", () => { - const inputId = element.getAttribute("for"); - - if (!inputId) { - console.log(element, element.getAttribute("for")); - throw "Input id in label not found"; - } - - const input = window.document.getElementById(inputId); - - if (!input || !("value" in input)) { - throw "Not input or no value"; - } - - const frame = window.document.getElementById( - /** @type {string} */ (input.value), - ); - - if (!frame) { - console.log(input.value); - throw "Frame element doesn't exist"; - } - - if (frame === focusedFrame) { - return; - } - - frame.hidden = false; - if (focusedFrame) { - focusedFrame.hidden = true; - } - focusedFrame = frame; - }); - break; - } - } - } - - elements.asideLabel.click(); - - // When going from mobile view to desktop view, if selected frame was open, go to the nav frame - new IntersectionObserver((entries) => { - for (let i = 0; i < entries.length; i++) { - if ( - !entries[i].isIntersecting && - entries[i].target === elements.asideLabel && - focusedFrame == elements.aside - ) { - elements.navLabel.click(); - } - } - }).observe(elements.asideLabel); - - function setAsideParent() { - const { clientWidth } = window.document.documentElement; - const { aside, body, main } = elements; - if (clientWidth >= consts.MEDIUM_WIDTH) { - aside.parentElement !== body && body.append(aside); - } else { - aside.parentElement !== main && main.append(aside); - } - } - - setAsideParent(); - - window.addEventListener("resize", setAsideParent); -} -initFrameSelectors(); - -function createKeyDownEventListener() { - window.document.addEventListener("keydown", (event) => { - switch (event.key) { - case "Escape": { - event.stopPropagation(); - event.preventDefault(); - elements.navLabel.click(); - break; - } - case "/": { - if (window.document.activeElement === elements.searchInput) { - return; - } - - event.stopPropagation(); - event.preventDefault(); - elements.searchLabel.click(); - elements.searchInput.focus(); - break; - } - } +function createIds() { + return /** @type {const} */ ({ + selectedId: `selected-id`, + asideSelectorLabel: `aside-selector-label`, + checkedFrameSelectorLabel: "checked-frame-selector-label", }); } -createKeyDownEventListener(); +/** @typedef {ReturnType} Ids */ + +/** + * @param {Ids} ids + */ +function getElements(ids) { + /** + * @param {string} id + */ + function getElementById(id) { + const element = window.document.getElementById(id); + if (!element) throw `Element with id = "${id}" should exist`; + return element; + } + + return { + head: window.document.getElementsByTagName("head")[0], + body: window.document.body, + main: getElementById("main"), + aside: getElementById("aside"), + asideLabel: getElementById(ids.asideSelectorLabel), + navLabel: getElementById(`nav-selector-label`), + searchLabel: getElementById(`search-selector-label`), + search: getElementById("search"), + nav: getElementById("nav"), + navHeader: getElementById("nav-header"), + searchInput: /** @type {HTMLInputElement} */ ( + getElementById("search-input") + ), + searchSmall: getElementById("search-small"), + searchResults: getElementById("search-results"), + selectors: getElementById("frame-selectors"), + style: getComputedStyle(window.document.documentElement), + charts: getElementById("charts"), + simulation: getElementById("simulation"), + livePrice: getElementById("live-price"), + moscowTime: getElementById("moscow-time"), + }; +} +/** @typedef {ReturnType} Elements */ /** * @param {Accessor} dark + * @param {Elements} elements + * @param {Utilities} utils */ -function createColors(dark) { +function createColors(dark, elements, utils) { /** * @param {string} color */ @@ -1568,8 +1516,10 @@ function createColors(dark) { /** * @param {Signals} signals + * @param {Constants} consts + * @param {Utilities} utils */ -function createDatasets(signals) { +function createDatasets(signals, consts, utils) { /** @type {Map>} */ const date = new Map(); /** @type {Map>} */ @@ -1932,8 +1882,9 @@ function createDatasets(signals) { /** * @param {Signals} signals + * @param {Utilities} utils */ -function initWebSockets(signals) { +function initWebSockets(signals, utils) { /** * @template T * @param {(callback: (value: T) => void) => WebSocket} creator @@ -2072,626 +2023,753 @@ function initWebSockets(signals) { } /** @typedef {ReturnType} WebSockets */ -packages.signals().then((signals) => - options.then(({ initOptions }) => { - function initDark() { - const preferredColorSchemeMatchMedia = window.matchMedia( - "(prefers-color-scheme: dark)", - ); - const dark = signals.createSignal(preferredColorSchemeMatchMedia.matches); - preferredColorSchemeMatchMedia.addEventListener( - "change", - ({ matches }) => { - dark.set(matches); - }, - ); - return dark; - } - const dark = initDark(); +function main() { + const packages = initPackages(); + const options = import("./options.js"); + const env = initEnv(); + const consts = createConstants(); + const utils = createUtils(); + const ids = createIds(); + const elements = getElements(ids); - const qrcode = signals.createSignal(/** @type {string | null} */ (null)); + function initFrameSelectors() { + const children = Array.from(elements.selectors.children); - function createLastHeightResource() { - const lastHeight = signals.createSignal(0); + /** @type {HTMLElement | undefined} */ + let focusedFrame = undefined; - function fetchLastHeight() { - fetch("/api/last-height").then((response) => { - response.json().then((json) => { - if (typeof json === "number") { - lastHeight.set(json); + for (let i = 0; i < children.length; i++) { + const element = children[i]; + + switch (element.tagName) { + case "LABEL": { + element.addEventListener("click", () => { + const inputId = element.getAttribute("for"); + + if (!inputId) { + console.log(element, element.getAttribute("for")); + throw "Input id in label not found"; } + + const input = window.document.getElementById(inputId); + + if (!input || !("value" in input)) { + throw "Not input or no value"; + } + + const frame = window.document.getElementById( + /** @type {string} */ (input.value), + ); + + if (!frame) { + console.log(input.value); + throw "Frame element doesn't exist"; + } + + if (frame === focusedFrame) { + return; + } + + frame.hidden = false; + if (focusedFrame) { + focusedFrame.hidden = true; + } + focusedFrame = frame; }); - }); + break; + } } - fetchLastHeight(); - setInterval(fetchLastHeight, consts.TEN_SECONDS_IN_MS, {}); - - return lastHeight; } - const lastHeight = createLastHeightResource(); - const lastValues = signals.createSignal(/** @type {LastValues} */ (null)); + elements.asideLabel.click(); - function createFetchLastValuesWhenNeededEffect() { - let previousHeight = -1; - signals.createEffect(lastHeight, (lastHeight) => { - if (previousHeight !== lastHeight) { - fetch("/api/last").then((response) => { + // When going from mobile view to desktop view, if selected frame was open, go to the nav frame + new IntersectionObserver((entries) => { + for (let i = 0; i < entries.length; i++) { + if ( + !entries[i].isIntersecting && + entries[i].target === elements.asideLabel && + focusedFrame == elements.aside + ) { + elements.navLabel.click(); + } + } + }).observe(elements.asideLabel); + + function setAsideParent() { + const { clientWidth } = window.document.documentElement; + const { aside, body, main } = elements; + if (clientWidth >= consts.MEDIUM_WIDTH) { + aside.parentElement !== body && body.append(aside); + } else { + aside.parentElement !== main && main.append(aside); + } + } + + setAsideParent(); + + window.addEventListener("resize", setAsideParent); + } + initFrameSelectors(); + + function createKeyDownEventListener() { + window.document.addEventListener("keydown", (event) => { + switch (event.key) { + case "Escape": { + event.stopPropagation(); + event.preventDefault(); + elements.navLabel.click(); + break; + } + case "/": { + if (window.document.activeElement === elements.searchInput) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + elements.searchLabel.click(); + elements.searchInput.focus(); + break; + } + } + }); + } + createKeyDownEventListener(); + + packages.signals().then((signals) => + options.then(({ initOptions }) => { + function initDark() { + const preferredColorSchemeMatchMedia = window.matchMedia( + "(prefers-color-scheme: dark)", + ); + const dark = signals.createSignal( + preferredColorSchemeMatchMedia.matches, + ); + preferredColorSchemeMatchMedia.addEventListener( + "change", + ({ matches }) => { + dark.set(matches); + }, + ); + return dark; + } + const dark = initDark(); + + const qrcode = signals.createSignal(/** @type {string | null} */ (null)); + + function createLastHeightResource() { + const lastHeight = signals.createSignal(0); + + function fetchLastHeight() { + fetch("/api/last-height").then((response) => { response.json().then((json) => { - if (typeof json === "object") { - lastValues.set(json); - previousHeight = lastHeight; + if (typeof json === "number") { + lastHeight.set(json); } }); }); } + fetchLastHeight(); + setInterval(fetchLastHeight, consts.TEN_SECONDS_IN_MS, {}); + + return lastHeight; + } + const lastHeight = createLastHeightResource(); + + const lastValues = signals.createSignal(/** @type {LastValues} */ (null)); + + function createFetchLastValuesWhenNeededEffect() { + let previousHeight = -1; + signals.createEffect(lastHeight, (lastHeight) => { + if (previousHeight !== lastHeight) { + fetch("/api/last").then((response) => { + response.json().then((json) => { + if (typeof json === "object") { + lastValues.set(json); + previousHeight = lastHeight; + } + }); + }); + } + }); + } + createFetchLastValuesWhenNeededEffect(); + + const webSockets = initWebSockets(signals, utils); + + const colors = createColors(dark, elements, utils); + + const options = initOptions({ + colors, + env, + ids, + lastValues, + signals, + utils, + webSockets, + qrcode, }); - } - createFetchLastValuesWhenNeededEffect(); - const webSockets = initWebSockets(signals); + // const urlSelected = utils.url.pathnameToSelectedId(); + // function createWindowPopStateEvent() { + // window.addEventListener("popstate", (event) => { + // const urlSelected = utils.url.pathnameToSelectedId(); + // const option = options.list.find((option) => urlSelected === option.id); + // if (option) { + // options.selected.set(option); + // } + // }); + // } + // createWindowPopStateEvent(); - const colors = createColors(dark); + function initSelected() { + function initSelectedFrame() { + console.log("selected: init"); - const options = initOptions({ - colors, - env, - ids, - lastValues, - signals, - utils, - webSockets, - qrcode, - }); + const datasets = createDatasets(signals, consts, utils); - function createWindowPopStateEvent() { - window.addEventListener("popstate", (event) => { - const urlSelected = utils.url.pathnameToSelectedId(); - const option = options.list.find((option) => urlSelected === option.id); - if (option) { - options.selected.set(option); + function createApplyOptionEffect() { + const lastChartOption = signals.createSignal( + /** @type {ChartOption | null} */ (null), + ); + const lastSimulationOption = signals.createSignal( + /** @type {SimulationOption | null} */ (null), + ); + + const owner = signals.getOwner(); + + let previousElement = /** @type {HTMLElement | undefined} */ ( + undefined + ); + let firstChartOption = true; + let firstSimulationOption = true; + let firstLivePriceOption = true; + let firstMoscowTimeOption = true; + + signals.createEffect(options.selected, (option) => { + if (previousElement) { + previousElement.hidden = true; + utils.url.resetParams(option); + utils.url.pushHistory(option.id); + } else { + utils.url.replaceHistory({ pathname: option.id }); + } + + /** @type {HTMLElement} */ + let element; + + switch (option.kind) { + // case "home": { + // element = elements.home; + // break; + // } + case "chart": { + console.log("chart", option); + + element = elements.charts; + + lastChartOption.set(option); + + if (firstChartOption) { + const lightweightCharts = packages.lightweightCharts(); + const chartScript = import("./chart.js"); + utils.dom.importStyleAndThen("/styles/chart.css", () => + chartScript.then(({ init: initChartsElement }) => + lightweightCharts.then((lightweightCharts) => + signals.runWithOwner(owner, () => + initChartsElement({ + colors, + datasets, + elements, + consts, + lightweightCharts, + selected: /** @type {any} */ (lastChartOption), + signals, + utils, + webSockets, + }), + ), + ), + ), + ); + } + firstChartOption = false; + + break; + } + case "simulation": { + element = elements.simulation; + + lastSimulationOption.set(option); + + if (firstSimulationOption) { + const lightweightCharts = packages.lightweightCharts(); + const simulationScript = import("./simulation.js"); + + utils.dom.importStyleAndThen("/styles/simulation.css", () => + simulationScript.then(({ init }) => + lightweightCharts.then((lightweightCharts) => + signals.runWithOwner(owner, () => + init({ + colors, + datasets, + elements, + lightweightCharts, + signals, + utils, + consts, + lastValues, + }), + ), + ), + ), + ); + } + firstSimulationOption = false; + + break; + } + case "live-price": { + console.log("live-price"); + + element = elements.livePrice; + + if (firstLivePriceOption) { + const lightweightCharts = packages.lightweightCharts(); + const script = import("./live-price.js"); + + utils.dom.importStyleAndThen("/styles/live-price.css", () => + script.then(({ init }) => + lightweightCharts.then((lightweightCharts) => + signals.runWithOwner(owner, () => + init({ + colors, + consts, + dark, + datasets, + elements, + ids, + lightweightCharts, + options, + signals, + utils, + webSockets, + }), + ), + ), + ), + ); + } + firstLivePriceOption = false; + + break; + } + case "moscow-time": { + console.log("moscow-time"); + + element = elements.moscowTime; + + if (firstLivePriceOption) { + const lightweightCharts = packages.lightweightCharts(); + const script = import("./moscow-time.js"); + + utils.dom.importStyleAndThen( + "/styles/moscow-time.css", + () => + script.then(({ init }) => + signals.runWithOwner(owner, () => + init({ + colors, + consts, + dark, + datasets, + elements, + ids, + options, + signals, + utils, + webSockets, + }), + ), + ), + ); + } + firstLivePriceOption = false; + + break; + } + case "converter": + case "home": + case "pdf": + case "url": { + return; + } + } + + element.hidden = false; + previousElement = element; + }); + } + createApplyOptionEffect(); } - }); - } - // createWindowPopStateEvent(); - function initSelected() { - function initSelectedFrame() { - console.log("selected: init"); - - const datasets = createDatasets(signals); - - function createApplyOptionEffect() { - const lastChartOption = signals.createSignal( - /** @type {ChartOption | null} */ (null), - ); - const lastSimulationOption = signals.createSignal( - /** @type {SimulationOption | null} */ (null), - ); - - const owner = signals.getOwner(); - - let previousElement = /** @type {HTMLElement | undefined} */ ( - undefined - ); - let firstChartOption = true; - let firstSimulationOption = true; - let firstLivePriceOption = true; - let firstMoscowTimeOption = true; - - signals.createEffect(options.selected, (option) => { - if (previousElement) { - previousElement.hidden = true; - utils.url.resetParams(option); - utils.url.pushHistory(option.id); - } else { - utils.url.replaceHistory({ pathname: option.id }); + function createMobileSwitchEffect() { + let firstRun = true; + signals.createEffect(options.selected, () => { + if (!firstRun && !utils.dom.isHidden(elements.asideLabel)) { + elements.asideLabel.click(); } - - /** @type {HTMLElement} */ - let element; - - switch (option.kind) { - // case "home": { - // element = elements.home; - // break; - // } - case "chart": { - console.log("chart", option); - - element = elements.charts; - - lastChartOption.set(option); - - if (firstChartOption) { - const lightweightCharts = packages.lightweightCharts(); - const chartScript = import("./chart.js"); - utils.dom.importStyleAndThen("/styles/chart.css", () => - chartScript.then(({ init: initChartsElement }) => - lightweightCharts.then((lightweightCharts) => - signals.runWithOwner(owner, () => - initChartsElement({ - colors, - datasets, - elements, - lightweightCharts, - selected: /** @type {any} */ (lastChartOption), - signals, - utils, - webSockets, - }), - ), - ), - ), - ); - } - firstChartOption = false; - - break; - } - case "simulation": { - element = elements.simulation; - - lastSimulationOption.set(option); - - if (firstSimulationOption) { - const lightweightCharts = packages.lightweightCharts(); - const simulationScript = import("./simulation.js"); - - utils.dom.importStyleAndThen("/styles/simulation.css", () => - simulationScript.then(({ init }) => - lightweightCharts.then((lightweightCharts) => - signals.runWithOwner(owner, () => - init({ - colors, - datasets, - elements, - lightweightCharts, - signals, - utils, - lastValues, - }), - ), - ), - ), - ); - } - firstSimulationOption = false; - - break; - } - case "live-price": { - console.log("live-price"); - - element = elements.livePrice; - - if (firstLivePriceOption) { - const lightweightCharts = packages.lightweightCharts(); - const script = import("./live-price.js"); - - utils.dom.importStyleAndThen("/styles/live-price.css", () => - script.then(({ init }) => - lightweightCharts.then((lightweightCharts) => - signals.runWithOwner(owner, () => - init({ - colors, - consts, - dark, - datasets, - elements, - ids, - lightweightCharts, - options, - signals, - utils, - webSockets, - }), - ), - ), - ), - ); - } - firstLivePriceOption = false; - - break; - } - case "moscow-time": { - console.log("moscow-time"); - - element = elements.moscowTime; - - if (firstLivePriceOption) { - const lightweightCharts = packages.lightweightCharts(); - const script = import("./moscow-time.js"); - - utils.dom.importStyleAndThen("/styles/moscow-time.css", () => - script.then(({ init }) => - signals.runWithOwner(owner, () => - init({ - colors, - consts, - dark, - datasets, - elements, - ids, - options, - signals, - utils, - webSockets, - }), - ), - ), - ); - } - firstLivePriceOption = false; - - break; - } - case "converter": - case "home": - case "pdf": - case "url": { - return; - } - } - - element.hidden = false; - previousElement = element; + firstRun = false; }); } - createApplyOptionEffect(); + createMobileSwitchEffect(); + + utils.dom.onFirstIntersection(elements.aside, initSelectedFrame); } + initSelected(); - function createMobileSwitchEffect() { - let firstRun = true; - signals.createEffect(options.selected, () => { - if (!firstRun && !utils.dom.isHidden(elements.asideLabel)) { - elements.asideLabel.click(); - } - firstRun = false; + function initShare() { + const shareDiv = utils.dom.getElementById("share-div"); + const shareContentDiv = utils.dom.getElementById("share-content-div"); + + shareDiv.addEventListener("click", () => { + qrcode.set(null); }); - } - createMobileSwitchEffect(); - utils.dom.onFirstIntersection(elements.aside, initSelectedFrame); - } - initSelected(); - - function initShare() { - const shareDiv = utils.dom.getElementById("share-div"); - const shareContentDiv = utils.dom.getElementById("share-content-div"); - - shareDiv.addEventListener("click", () => { - qrcode.set(null); - }); - - shareContentDiv.addEventListener("click", (event) => { - event.stopPropagation(); - event.preventDefault(); - }); - - packages.leanQr().then(({ generate }) => { - const imgQrcode = /** @type {HTMLImageElement} */ ( - utils.dom.getElementById("share-img") - ); - - const anchor = /** @type {HTMLAnchorElement} */ ( - utils.dom.getElementById("share-anchor") - ); - - signals.createEffect(qrcode, (qrcode) => { - if (!qrcode) { - shareDiv.hidden = true; - return; - } - - const href = qrcode; - anchor.href = href; - anchor.innerText = - (href.startsWith("http") - ? href.split("//").at(-1) - : href.split(":").at(-1)) || ""; - - imgQrcode.src = - generate(/** @type {any} */ (href))?.toDataURL({ - // @ts-ignore - padX: 0, - padY: 0, - }) || ""; - - shareDiv.hidden = false; + shareContentDiv.addEventListener("click", (event) => { + event.stopPropagation(); + event.preventDefault(); }); - }); - } - initShare(); - function initFolders() { - function initTreeElement() { - options.treeElement.set(() => { - const treeElement = window.document.createElement("div"); - treeElement.classList.add("tree"); - elements.navHeader.after(treeElement); - return treeElement; - }); - } + packages.leanQr().then(({ generate }) => { + const imgQrcode = /** @type {HTMLImageElement} */ ( + utils.dom.getElementById("share-img") + ); - async function scrollToSelected() { - if (!options.selected()) throw "Selected should be set by now"; - const selectedId = options.selected().id; + const anchor = /** @type {HTMLAnchorElement} */ ( + utils.dom.getElementById("share-anchor") + ); - const path = options.selected().path; - - let i = 0; - while (i !== path.length) { - try { - const id = path[i].id; - const details = /** @type {HTMLDetailsElement} */ ( - utils.dom.getElementById(id) - ); - details.open = true; - i++; - } catch { - await utils.yield(); - } - } - - await utils.yield(); - - utils.dom.getElementById(`${selectedId}-nav-selector`).scrollIntoView({ - behavior: "instant", - block: "center", - }); - } - - utils.dom.onFirstIntersection(elements.nav, () => { - console.log("nav: init"); - initTreeElement(); - scrollToSelected(); - }); - } - initFolders(); - - function initSearch() { - function initSearchFrame() { - console.log("search: init"); - - const haystack = options.list.map( - (option) => `${option.title}\t${option.serializedPath}`, - ); - - const searchSmallOgInnerHTML = elements.searchSmall.innerHTML; - - const RESULTS_PER_PAGE = 100; - - packages.ufuzzy().then((ufuzzy) => { - /** - * @param {uFuzzy.SearchResult} searchResult - * @param {number} pageIndex - */ - function computeResultPage(searchResult, pageIndex) { - /** @type {{ option: Option, path: string, title: string }[]} */ - let list = []; - - let [indexes, info, order] = searchResult || [null, null, null]; - - const minIndex = pageIndex * RESULTS_PER_PAGE; - - if (indexes?.length) { - const maxIndex = Math.min( - (order || indexes).length - 1, - minIndex + RESULTS_PER_PAGE - 1, - ); - - list = Array(maxIndex - minIndex + 1); - - for (let i = minIndex; i <= maxIndex; i++) { - let index = indexes[i]; - - const [title, path] = haystack[index].split("\t"); - - list[i % 100] = { - option: options.list[index], - path, - title, - }; - } + signals.createEffect(qrcode, (qrcode) => { + if (!qrcode) { + shareDiv.hidden = true; + return; } - return list; + const href = qrcode; + anchor.href = href; + anchor.innerText = + (href.startsWith("http") + ? href.split("//").at(-1) + : href.split(":").at(-1)) || ""; + + imgQrcode.src = + generate(/** @type {any} */ (href))?.toDataURL({ + // @ts-ignore + padX: 0, + padY: 0, + }) || ""; + + shareDiv.hidden = false; + }); + }); + } + initShare(); + + function initFolders() { + function initTreeElement() { + options.treeElement.set(() => { + const treeElement = window.document.createElement("div"); + treeElement.classList.add("tree"); + elements.navHeader.after(treeElement); + return treeElement; + }); + } + + async function scrollToSelected() { + if (!options.selected()) throw "Selected should be set by now"; + const selectedId = options.selected().id; + + const path = options.selected().path; + + let i = 0; + while (i !== path.length) { + try { + const id = path[i].id; + const details = /** @type {HTMLDetailsElement} */ ( + utils.dom.getElementById(id) + ); + details.open = true; + i++; + } catch { + await utils.next(); + } } - /** @type {uFuzzy.Options} */ - const config = { - intraIns: Infinity, - intraChars: `[a-z\d' ]`, - }; + await utils.next(); - const fuzzyMultiInsert = /** @type {uFuzzy} */ ( - ufuzzy({ - intraIns: 1, - }) - ); - const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ ( - ufuzzy(config) - ); - const fuzzySingleError = /** @type {uFuzzy} */ ( - ufuzzy({ - intraMode: 1, - ...config, - }) - ); - const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ ( - ufuzzy({ - intraMode: 1, - ...config, - }) + utils.dom + .getElementById(`${selectedId}-nav-selector`) + .scrollIntoView({ + behavior: "instant", + block: "center", + }); + } + + utils.dom.onFirstIntersection(elements.nav, () => { + console.log("nav: init"); + initTreeElement(); + scrollToSelected(); + }); + } + initFolders(); + + function initSearch() { + function initSearchFrame() { + console.log("search: init"); + + const haystack = options.list.map( + (option) => `${option.title}\t${option.serializedPath}`, ); - /** @type {VoidFunction | undefined} */ - let dispose; + const searchSmallOgInnerHTML = elements.searchSmall.innerHTML; - function inputEvent() { - signals.createRoot((_dispose) => { - const needle = /** @type {string} */ (elements.searchInput.value); + const RESULTS_PER_PAGE = 100; - dispose?.(); + packages.ufuzzy().then((ufuzzy) => { + /** + * @param {uFuzzy.SearchResult} searchResult + * @param {number} pageIndex + */ + function computeResultPage(searchResult, pageIndex) { + /** @type {{ option: Option, path: string, title: string }[]} */ + let list = []; - dispose = _dispose; + let [indexes, info, order] = searchResult || [null, null, null]; - elements.searchResults.scrollTo({ - top: 0, - }); + const minIndex = pageIndex * RESULTS_PER_PAGE; - if (!needle) { - elements.searchSmall.innerHTML = searchSmallOgInnerHTML; - elements.searchResults.innerHTML = ""; - return; - } - - const outOfOrder = 5; - const infoThresh = 5_000; - - let result = fuzzyMultiInsert?.search( - haystack, - needle, - undefined, - infoThresh, - ); - - if (!result?.[0]?.length || !result?.[1]) { - result = fuzzyMultiInsert?.search( - haystack, - needle, - outOfOrder, - infoThresh, + if (indexes?.length) { + const maxIndex = Math.min( + (order || indexes).length - 1, + minIndex + RESULTS_PER_PAGE - 1, ); + + list = Array(maxIndex - minIndex + 1); + + for (let i = minIndex; i <= maxIndex; i++) { + let index = indexes[i]; + + const [title, path] = haystack[index].split("\t"); + + list[i % 100] = { + option: options.list[index], + path, + title, + }; + } } - if (!result?.[0]?.length || !result?.[1]) { - result = fuzzySingleError?.search( - haystack, - needle, - outOfOrder, - infoThresh, + return list; + } + + /** @type {uFuzzy.Options} */ + const config = { + intraIns: Infinity, + intraChars: `[a-z\d' ]`, + }; + + const fuzzyMultiInsert = /** @type {uFuzzy} */ ( + ufuzzy({ + intraIns: 1, + }) + ); + const fuzzyMultiInsertFuzzier = /** @type {uFuzzy} */ ( + ufuzzy(config) + ); + const fuzzySingleError = /** @type {uFuzzy} */ ( + ufuzzy({ + intraMode: 1, + ...config, + }) + ); + const fuzzySingleErrorFuzzier = /** @type {uFuzzy} */ ( + ufuzzy({ + intraMode: 1, + ...config, + }) + ); + + /** @type {VoidFunction | undefined} */ + let dispose; + + function inputEvent() { + signals.createRoot((_dispose) => { + const needle = /** @type {string} */ ( + elements.searchInput.value ); - } - if (!result?.[0]?.length || !result?.[1]) { - result = fuzzySingleErrorFuzzier?.search( - haystack, - needle, - outOfOrder, - infoThresh, - ); - } + dispose?.(); - if (!result?.[0]?.length || !result?.[1]) { - result = fuzzyMultiInsertFuzzier?.search( + dispose = _dispose; + + elements.searchResults.scrollTo({ + top: 0, + }); + + if (!needle) { + elements.searchSmall.innerHTML = searchSmallOgInnerHTML; + elements.searchResults.innerHTML = ""; + return; + } + + const outOfOrder = 5; + const infoThresh = 5_000; + + let result = fuzzyMultiInsert?.search( haystack, needle, undefined, infoThresh, ); - } - if (!result?.[0]?.length || !result?.[1]) { - result = fuzzyMultiInsertFuzzier?.search( - haystack, - needle, - outOfOrder, - infoThresh, - ); - } - - elements.searchSmall.innerHTML = `Found ${ - result?.[0]?.length || 0 - } result(s)`; - elements.searchResults.innerHTML = ""; - - const list = computeResultPage(result, 0); - - list.forEach(({ option, path, title }) => { - const li = window.document.createElement("li"); - elements.searchResults.appendChild(li); - - const element = options.createOptionElement({ - option, - frame: "search", - name: title, - top: path, - qrcode, - }); - - if (element) { - li.append(element); + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsert?.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzySingleError?.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzySingleErrorFuzzier?.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsertFuzzier?.search( + haystack, + needle, + undefined, + infoThresh, + ); + } + + if (!result?.[0]?.length || !result?.[1]) { + result = fuzzyMultiInsertFuzzier?.search( + haystack, + needle, + outOfOrder, + infoThresh, + ); + } + + elements.searchSmall.innerHTML = `Found ${ + result?.[0]?.length || 0 + } result(s)`; + elements.searchResults.innerHTML = ""; + + const list = computeResultPage(result, 0); + + list.forEach(({ option, path, title }) => { + const li = window.document.createElement("li"); + elements.searchResults.appendChild(li); + + const element = options.createOptionElement({ + option, + frame: "search", + name: title, + top: path, + qrcode, + }); + + if (element) { + li.append(element); + } + }); }); - }); - } + } - if (elements.searchInput.value) { - inputEvent(); - } + if (elements.searchInput.value) { + inputEvent(); + } - elements.searchInput.addEventListener("input", inputEvent); + elements.searchInput.addEventListener("input", inputEvent); + }); + } + utils.dom.onFirstIntersection(elements.search, initSearchFrame); + } + initSearch(); + + function initDesktopResizeBar() { + const resizeBar = utils.dom.getElementById("resize-bar"); + let resize = false; + let startingWidth = 0; + let startingClientX = 0; + + const barWidthLocalStorageKey = "bar-width"; + + /** + * @param {number | null} width + */ + function setBarWidth(width) { + if (typeof width === "number") { + elements.main.style.width = `${width}px`; + localStorage.setItem(barWidthLocalStorageKey, String(width)); + } else { + elements.main.style.width = elements.style.getPropertyValue( + "--default-main-width", + ); + localStorage.removeItem(barWidthLocalStorageKey); + } + } + + /** + * @param {MouseEvent} event + */ + function mouseMoveEvent(event) { + if (resize) { + setBarWidth(startingWidth + (event.clientX - startingClientX)); + } + } + + resizeBar.addEventListener("mousedown", (event) => { + startingClientX = event.clientX; + startingWidth = elements.main.clientWidth; + resize = true; + window.document.documentElement.dataset.resize = ""; + window.addEventListener("mousemove", mouseMoveEvent); }); + + resizeBar.addEventListener("dblclick", () => { + setBarWidth(null); + }); + + const setResizeFalse = () => { + resize = false; + delete window.document.documentElement.dataset.resize; + window.removeEventListener("mousemove", mouseMoveEvent); + }; + window.addEventListener("mouseup", setResizeFalse); + window.addEventListener("mouseleave", setResizeFalse); } - utils.dom.onFirstIntersection(elements.search, initSearchFrame); - } - initSearch(); - - function initDesktopResizeBar() { - const resizeBar = utils.dom.getElementById("resize-bar"); - let resize = false; - let startingWidth = 0; - let startingClientX = 0; - - const barWidthLocalStorageKey = "bar-width"; - - /** - * @param {number | null} width - */ - function setBarWidth(width) { - if (typeof width === "number") { - elements.main.style.width = `${width}px`; - localStorage.setItem(barWidthLocalStorageKey, String(width)); - } else { - elements.main.style.width = elements.style.getPropertyValue( - "--default-main-width", - ); - localStorage.removeItem(barWidthLocalStorageKey); - } - } - - /** - * @param {MouseEvent} event - */ - function mouseMoveEvent(event) { - if (resize) { - setBarWidth(startingWidth + (event.clientX - startingClientX)); - } - } - - resizeBar.addEventListener("mousedown", (event) => { - startingClientX = event.clientX; - startingWidth = elements.main.clientWidth; - resize = true; - window.document.documentElement.dataset.resize = ""; - window.addEventListener("mousemove", mouseMoveEvent); - }); - - resizeBar.addEventListener("dblclick", () => { - setBarWidth(null); - }); - - const setResizeFalse = () => { - resize = false; - delete window.document.documentElement.dataset.resize; - window.removeEventListener("mousemove", mouseMoveEvent); - }; - window.addEventListener("mouseup", setResizeFalse); - window.addEventListener("mouseleave", setResizeFalse); - } - initDesktopResizeBar(); - }), -); + initDesktopResizeBar(); + }), + ); +} +main(); diff --git a/websites/kibo.money/scripts/moscow-time.js b/websites/kibo.money/scripts/moscow-time.js index a02d2e0f7..77fdcafff 100644 --- a/websites/kibo.money/scripts/moscow-time.js +++ b/websites/kibo.money/scripts/moscow-time.js @@ -7,7 +7,7 @@ /** * @param {Object} args * @param {Colors} args.colors - * @param {Consts} args.consts + * @param {Constants} args.consts * @param {Signals} args.signals * @param {Utilities} args.utils * @param {Options} args.options diff --git a/websites/kibo.money/scripts/options.js b/websites/kibo.money/scripts/options.js index 6d0c35637..f3a0ffb25 100644 --- a/websites/kibo.money/scripts/options.js +++ b/websites/kibo.money/scripts/options.js @@ -5,9 +5,6 @@ * @import {AnySpecificSeriesBlueprint, SplitSeriesBlueprint} from '../packages/lightweight-charts/types'; */ -const DATE_TO_PREFIX = "date-to-"; -const HEIGHT_TO_PREFIX = "height-to-"; - function initGroups() { const xTermHolders = /** @type {const} */ ([ { @@ -5204,8 +5201,8 @@ export function initOptions({ if (!blueprint) return undefined; const id = blueprint.datasetPath - .replace(DATE_TO_PREFIX, "") - .replace(HEIGHT_TO_PREFIX, ""); + .replace("date-to-", "") + .replace("height-to-", ""); return /** @type {LastPath} */ (id); } diff --git a/websites/kibo.money/scripts/simulation.js b/websites/kibo.money/scripts/simulation.js index 13b644c16..6e3b4327a 100644 --- a/websites/kibo.money/scripts/simulation.js +++ b/websites/kibo.money/scripts/simulation.js @@ -13,6 +13,7 @@ * @param {Utilities} args.utils * @param {Datasets} args.datasets * @param {Elements} args.elements + * @param {Constants} args.consts * @param {Signal} args.lastValues */ export function init({ @@ -22,6 +23,7 @@ export function init({ lightweightCharts, signals, utils, + consts, lastValues, }) { const simulationElement = elements.simulation; @@ -31,6 +33,120 @@ export function init({ const resultsElement = window.document.createElement("div"); simulationElement.append(resultsElement); + function computeFrequencies() { + const weekDays = [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday", + ]; + const maxDays = 28; + + /** @param {number} day */ + function getOrdinalDay(day) { + const rest = (day % 30) % 20; + + return `${day}${ + rest === 1 ? "st" : rest === 2 ? "nd" : rest === 3 ? "rd" : "th" + }`; + } + + /** @satisfies {([Frequency, Frequencies, Frequencies, Frequencies])} */ + const list = [ + { + name: "Every day", + value: "every-day", + /** @param {Date} _ */ + isTriggerDay(_) { + return true; + }, + }, + { + name: "Once a week", + list: weekDays.map((day, index) => ({ + name: day, + value: day.toLowerCase(), + /** @param {Date} date */ + isTriggerDay(date) { + let day = date.getUTCDay() - 1; + if (day === -1) { + day = 6; + } + return day === index; + }, + })), + }, + { + name: "Every two weeks", + list: [...Array(Math.round(maxDays / 2)).keys()].map((day) => { + const day1 = day + 1; + const day2 = day + 15; + + return { + value: `${day1}+${day2}`, + name: `The ${getOrdinalDay(day1)} and the ${getOrdinalDay(day2)}`, + /** @param {Date} date */ + isTriggerDay(date) { + const d = date.getUTCDate(); + return d === day1 || d === day2; + }, + }; + }), + }, + { + name: "Once a month", + list: [...Array(maxDays).keys()].map((day) => { + day++; + + return { + name: `The ${getOrdinalDay(day)}`, + value: String(day), + /** @param {Date} date */ + isTriggerDay(date) { + const d = date.getUTCDate(); + return d === day; + }, + }; + }), + }, + ]; + + /** @type {Record} */ + const idToFrequency = {}; + + list.forEach((anyFreq, index) => { + if ("list" in anyFreq) { + anyFreq.list?.forEach((freq) => { + idToFrequency[freq.value] = freq; + }); + } else { + idToFrequency[anyFreq.value] = anyFreq; + } + }); + + const serde = { + /** + * @param {Frequency} v + */ + serialize(v) { + return v.value; + }, + /** + * @param {string} v + */ + deserialize(v) { + const freq = idToFrequency[v]; + if (!freq) throw "Freq not found"; + return freq; + }, + }; + + return { list, serde }; + } + const frequencies = computeFrequencies(); const keyPrefix = "save-in-bitcoin"; @@ -437,6 +553,7 @@ export function init({ kind: "static", scale: "date", utils, + consts, config: [ { unit: "US Dollars", @@ -478,6 +595,7 @@ export function init({ scale: "date", kind: "static", utils, + consts, config: [ { unit: "US Dollars", @@ -501,6 +619,7 @@ export function init({ scale: "date", kind: "static", utils, + consts, config: [ { unit: "US Dollars", @@ -530,6 +649,7 @@ export function init({ scale: "date", kind: "static", utils, + consts, config: [ { unit: "US Dollars", @@ -563,6 +683,7 @@ export function init({ scale: "date", utils, owner, + consts, config: [ { unit: "Percentage", @@ -879,117 +1000,3 @@ export function init({ }); }); } - -/** @param {number} day */ -function getOrdinalDay(day) { - const rest = (day % 30) % 20; - - return `${day}${ - rest === 1 ? "st" : rest === 2 ? "nd" : rest === 3 ? "rd" : "th" - }`; -} - -function computeFrequencies() { - const weekDays = [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ]; - const maxDays = 28; - - /** @satisfies {([Frequency, Frequencies, Frequencies, Frequencies])} */ - const list = [ - { - name: "Every day", - value: "every-day", - /** @param {Date} _ */ - isTriggerDay(_) { - return true; - }, - }, - { - name: "Once a week", - list: weekDays.map((day, index) => ({ - name: day, - value: day.toLowerCase(), - /** @param {Date} date */ - isTriggerDay(date) { - let day = date.getUTCDay() - 1; - if (day === -1) { - day = 6; - } - return day === index; - }, - })), - }, - { - name: "Every two weeks", - list: [...Array(Math.round(maxDays / 2)).keys()].map((day) => { - const day1 = day + 1; - const day2 = day + 15; - - return { - value: `${day1}+${day2}`, - name: `The ${getOrdinalDay(day1)} and the ${getOrdinalDay(day2)}`, - /** @param {Date} date */ - isTriggerDay(date) { - const d = date.getUTCDate(); - return d === day1 || d === day2; - }, - }; - }), - }, - { - name: "Once a month", - list: [...Array(maxDays).keys()].map((day) => { - day++; - - return { - name: `The ${getOrdinalDay(day)}`, - value: String(day), - /** @param {Date} date */ - isTriggerDay(date) { - const d = date.getUTCDate(); - return d === day; - }, - }; - }), - }, - ]; - - /** @type {Record} */ - const idToFrequency = {}; - - list.forEach((anyFreq, index) => { - if ("list" in anyFreq) { - anyFreq.list?.forEach((freq) => { - idToFrequency[freq.value] = freq; - }); - } else { - idToFrequency[anyFreq.value] = anyFreq; - } - }); - - const serde = { - /** - * @param {Frequency} v - */ - serialize(v) { - return v.value; - }, - /** - * @param {string} v - */ - deserialize(v) { - const freq = idToFrequency[v]; - if (!freq) throw "Freq not found"; - return freq; - }, - }; - - return { list, serde }; -}