From bc73d0d11ccc309e9f27e6333df7490cacd14b41 Mon Sep 17 00:00:00 2001 From: John Paul Rutigliano Date: Sat, 25 Nov 2023 19:30:59 -0500 Subject: [PATCH 01/11] Fix final-batch container elem fixed size set incorrectly --- public/ts/mod_search_logic.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index bca28ee..2a8095f 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -803,10 +803,14 @@ var results_persist = false; var LI_HEIGHT: number, BATCH_SIZE: number; // Add Stylesheet var sheet = createStyleSheet("mod-list-constructed"); +var computeLiHeightPx = (liHeight: number, batchSize: number) => { + const gap = 4; + return liHeight * batchSize + gap * (batchSize - 1); +}; function setLiHeight(liHeight: number) { LI_HEIGHT = liHeight; const gap = 4; - const height = LI_HEIGHT * BATCH_SIZE + gap * (BATCH_SIZE - 1); + const height = computeLiHeightPx(LI_HEIGHT, BATCH_SIZE); if (sheet.cssRules.length > 0) sheet.removeRule(); sheet.insertRule(`.item_batch { height: ${height}px; @@ -1006,8 +1010,11 @@ const storeBatches = ( useContainers = true ) => { const endIdx = startIdx + batchSize; - const data_batch = []; + const data_batch: Mod[] = []; const nextBatchSize = Math.min(batchSize, results.length - endIdx); + for (let i = startIdx; i < endIdx; i++) { + data_batch.push(results[i]); + } if (useContainers) { const batch_container = document.createElement("div"); batch_container.setAttribute("class", "item_batch"); @@ -1016,16 +1023,14 @@ const storeBatches = ( batch_containers.push(batch_container); resultsListElement.appendChild(batch_container); + if (nextBatchSize <= 0) { - batch_container.style.height = data_batch.length * LI_HEIGHT + "px"; - batch_container.style.minHeight = - data_batch.length * LI_HEIGHT + "px"; + const height = computeLiHeightPx(LI_HEIGHT, data_batch.length); + batch_container.style.height = height + "px"; + batch_container.style.minHeight = height + "px"; } } - for (let i = startIdx; i < endIdx; i++) { - data_batch.push(results[i]); - } data_batches.push(data_batch); if (nextBatchSize > 0) From 29b2665a893cd62ebeb2d2a39ab9290c15ed142c Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sat, 1 Mar 2025 14:43:56 -0500 Subject: [PATCH 02/11] introduce civet experiment, add pnpm and package.json for scripts --- package.json | 18 +++++ pnpm-lock.yaml | 122 ++++++++++++++++++++++++++++++ public/ts/list_item_shared.ts | 14 ++-- public/ts/list_search.civet | 136 ++++++++++++++++++++++++++++++++++ public/ts/list_search.ts | 33 ++++----- public/ts/multiselect.civet | 118 +++++++++++++++++++++++++++++ public/ts/multiselect.ts | 57 ++++++++------ tsconfig.json | 4 +- 8 files changed, 450 insertions(+), 52 deletions(-) create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 public/ts/list_search.civet create mode 100644 public/ts/multiselect.civet diff --git a/package.json b/package.json new file mode 100644 index 0000000..c8fd15e --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "public", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "civet": "civet -c public/ts/*.civet -o .ts", + "build": "tsc", + "__build:civet": "tsc && civet --js -c public/ts/*.civet -o ./public/ts-compiled/.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@danielx/civet": "^0.9.7" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f901b5b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,122 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@danielx/civet': + specifier: ^0.9.7 + version: 0.9.7(typescript@5.8.2) + +packages: + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@danielx/civet@0.9.7': + resolution: {integrity: sha512-T3rCaTMMWnQsII9MYL8ZTfFAnf738gXihNcHZXb/zKRqcKUR28mfnjbsv8a/gC90Dr+cnlq8WrIu9BaDyfU0Bw==} + engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} + hasBin: true + peerDependencies: + typescript: ^4.5 || ^5.0 + yaml: ^2.4.5 + peerDependenciesMeta: + yaml: + optional: true + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@typescript/vfs@1.6.1': + resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} + peerDependencies: + typescript: '*' + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + unplugin@2.2.0: + resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} + engines: {node: '>=18.12.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + +snapshots: + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@danielx/civet@0.9.7(typescript@5.8.2)': + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@typescript/vfs': 1.6.1(typescript@5.8.2) + typescript: 5.8.2 + unplugin: 2.2.0 + transitivePeerDependencies: + - supports-color + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@typescript/vfs@1.6.1(typescript@5.8.2)': + dependencies: + debug: 4.4.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + + acorn@8.14.0: {} + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + ms@2.1.3: {} + + typescript@5.8.2: {} + + unplugin@2.2.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + + webpack-virtual-modules@0.6.2: {} diff --git a/public/ts/list_item_shared.ts b/public/ts/list_item_shared.ts index e527699..2746978 100644 --- a/public/ts/list_item_shared.ts +++ b/public/ts/list_item_shared.ts @@ -25,10 +25,6 @@ var authorListPopup = (function createAuthorListDiv() { return contentDiv; })(); -/** - * - * @param {HTMLElement} node - */ function clearChildren(node: HTMLElement) { while (node.hasChildNodes()) { node.removeChild(node.firstChild!); @@ -51,7 +47,7 @@ function showAuthorList( hoverTrigger?: { element: HTMLElement; listener: (e: MouseEvent) => void; - } + }, ) { clearChildren(authorListPopup); @@ -72,7 +68,7 @@ function showAuthorList( listDiv.classList.remove("hidden"); hoverTrigger?.element.removeEventListener( "mouseover", - hoverTrigger.listener + hoverTrigger.listener, ); const handleExit = () => { @@ -80,7 +76,7 @@ function showAuthorList( listDiv.classList.add("hidden"); hoverTrigger?.element.addEventListener( "mouseover", - hoverTrigger.listener + hoverTrigger.listener, ); document.body.removeEventListener("click", handleExit); }; @@ -130,7 +126,7 @@ export function fillAuthorDiv(authorDiv: HTMLDivElement, modData: Mod) { { element: authorDiv, listener: hoverListener, - } + }, ); }; authorDiv.addEventListener("mouseover", hoverListener); @@ -144,7 +140,7 @@ export function fillAuthorDiv(authorDiv: HTMLDivElement, modData: Mod) { authorListPopup, modData.authors, textRect.x, - textRect.y + textRect.y, ); e.stopPropagation(); diff --git a/public/ts/list_search.civet b/public/ts/list_search.civet new file mode 100644 index 0000000..2054bf0 --- /dev/null +++ b/public/ts/list_search.civet @@ -0,0 +1,136 @@ +import { DefaultListElementRenderer } from "./list_elem_default.js"; +import { DetailedListElementRenderer } from "./list_elem_detailed.js"; +import { setModCategoryElements } from "./list_item_shared.js"; +import { + CATEGORIES, + batch_containers, + init, + initSearch, + loader, + resultsListElement, + setLiHeight, + setResultsListElement, +} from "./mod_search_logic.js"; +import { Mod } from "./mod_types.js"; +import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; + +type ListElementRenderFn = (modData: Mod) => HTMLLIElement; + +const createListElement: ListElementRenderFn = DefaultListElementRenderer(); + +const createListElementDetailed: ListElementRenderFn = + DetailedListElementRenderer(); + +function createBatch(batchIdx: number, data_batches: Mod[][]): void { + for (const result_data of data_batches[batchIdx]) { + try { + batch_containers[batchIdx].appendChild( + currentListCreationFunc(result_data), + ); + } catch (err) { + batch_containers[batchIdx] + .appendChild(document.createElement("li")) + .setAttribute("class", "item"); + + console.warn(err); + console.warn(result_data); + } + }; +} + +// // Logic createListElement, true, LI_HEIGHT, BATCH_SIZE, createBatch +// type ModWithElem = Mod & { +// elem: HTMLElement; +// _elem: HTMLElement; +// _elemFn: () => HTMLElement; +// getElem: () => HTMLElement; +// }; +// +// loader.addCompletionFunc(() => { +// const MAX_FAILS = 100; +// let failCount = 0; +// for (const mod of mod_data as ModWithElem[]) { +// try { +// // mod.elem = createListElement(mod); +// // mod._elemFn = () => mod._elem = createListElement(mod); +// } catch (err) { +// console.warn(`Could not load elem for mod`); +// failCount++; +// if (failCount > MAX_FAILS) { +// break; +// } +// } +// } +// }); + +loader.addCompletionFunc(() => + setResultsListElement(getElementById("search_results_list")); +); + +var modCategoryElements: (() => Node)[]; +loader.addCompletionFunc(() => { + modCategoryElements = CATEGORIES + .map((c) => document.createElement("li") ||> .textContent = c.name) + .map((c) => () => c.cloneNode(true)); + + setModCategoryElements(modCategoryElements); +}); + +function getLiHeight() { + const fmt = (val: string) => val.slice(0, val.length - 2) + const style = getComputedStyle(resultsListElement); + return parseInt(fmt(style.getPropertyValue("--item-height"))) + 2; +} + +function getLiHeightDetailed() { + const fmt = (val: string) => val.slice(0, val.length - 2) + const style = getComputedStyle(resultsListElement); + + return parseInt(fmt(style.getPropertyValue("--item-height"))) + 36 + 8 + 2; +} + +var viewModes = [createListElement, createListElementDetailed]; +var modeLiHeights = [getLiHeight, getLiHeightDetailed]; +const CURRENT_LIST_VIEW_IDX_STORAGE_KEY = "currentListViewIdx"; +var currentViewIdx = + Number(window.localStorage.getItem(CURRENT_LIST_VIEW_IDX_STORAGE_KEY)) ?? 0; +var currentListCreationFunc = viewModes[0]; +function updateViewModes() { + currentListCreationFunc = viewModes[currentViewIdx]; + setLiHeight(modeLiHeights[currentViewIdx]()); +} +function cycleListViewModes() { + currentViewIdx++; + if (currentViewIdx >= viewModes.length) { + currentViewIdx = 0; + } + window.localStorage.setItem( + CURRENT_LIST_VIEW_IDX_STORAGE_KEY, + currentViewIdx.toString(), + ); + updateViewModes(); +} + +window.matchMedia("only screen and (max-width: 1000px)").onchange = () => { + updateViewModes(); +}; + +executeIfWhenDOMContentLoaded(() => { + getElementById("list_view_cycle_button").addEventListener( + "click", + cycleListViewModes, + ); +}); + +loader.addCompletionFunc(() => { + initSearch({ + results_persist: true, + batchCreationFunc: createBatch, + lazyLoadBatches: true, + batch_size: 20, + li_height: modeLiHeights[currentViewIdx](), + }); + updateViewModes(); +}); + +init(); diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index 8a25549..be5ded1 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -21,11 +21,11 @@ const createListElement: ListElementRenderFn = DefaultListElementRenderer(); const createListElementDetailed: ListElementRenderFn = DetailedListElementRenderer(); -var createBatch = (batchIdx: number, data_batches: Mod[][]) => { +function createBatch(batchIdx: number, data_batches: Mod[][]): void { for (const result_data of data_batches[batchIdx]) { try { batch_containers[batchIdx].appendChild( - currentListCreationFunc(result_data) + currentListCreationFunc(result_data), ); } catch (err) { batch_containers[batchIdx] @@ -35,8 +35,8 @@ var createBatch = (batchIdx: number, data_batches: Mod[][]) => { console.warn(err); console.warn(result_data); } - } -}; + }; +} // // Logic createListElement, true, LI_HEIGHT, BATCH_SIZE, createBatch // type ModWithElem = Mod & { @@ -63,31 +63,28 @@ var createBatch = (batchIdx: number, data_batches: Mod[][]) => { // } // }); -loader.addCompletionFunc(() => - setResultsListElement(getElementById("search_results_list")) +loader.addCompletionFunc(() => { + setResultsListElement(getElementById("search_results_list")); +} ); var modCategoryElements: (() => Node)[]; loader.addCompletionFunc(() => { - modCategoryElements = (() => { - const categoryReferenceElements = CATEGORIES.map((c) => { - const catElem = document.createElement("li"); - catElem.textContent = c.name; - return catElem; - }); - return categoryReferenceElements.map((c) => () => c.cloneNode(true)); - })(); + modCategoryElements = CATEGORIES + .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) + .map((c) => () => c.cloneNode(true)); + setModCategoryElements(modCategoryElements); }); function getLiHeight() { - const fmt = (val: string) => val.slice(0, val.length - 2); + const fmt = (val: string) => val.slice(0, val.length - 2) const style = getComputedStyle(resultsListElement); return parseInt(fmt(style.getPropertyValue("--item-height"))) + 2; } function getLiHeightDetailed() { - const fmt = (val: string) => val.slice(0, val.length - 2); + const fmt = (val: string) => val.slice(0, val.length - 2) const style = getComputedStyle(resultsListElement); return parseInt(fmt(style.getPropertyValue("--item-height"))) + 36 + 8 + 2; @@ -110,7 +107,7 @@ function cycleListViewModes() { } window.localStorage.setItem( CURRENT_LIST_VIEW_IDX_STORAGE_KEY, - currentViewIdx.toString() + currentViewIdx.toString(), ); updateViewModes(); } @@ -122,7 +119,7 @@ window.matchMedia("only screen and (max-width: 1000px)").onchange = () => { executeIfWhenDOMContentLoaded(() => { getElementById("list_view_cycle_button").addEventListener( "click", - cycleListViewModes + cycleListViewModes, ); }); diff --git a/public/ts/multiselect.civet b/public/ts/multiselect.civet new file mode 100644 index 0000000..20dc647 --- /dev/null +++ b/public/ts/multiselect.civet @@ -0,0 +1,118 @@ +type FiberElement = HTMLElement & { + _fibermc_initialized?: boolean; +}; + +type MultiSelectValueElement = HTMLLabelElement & { + _fibermc_optionValue: TValue; + _fibermc_onSelect: () => void; + _fibermc_setChecked: (checked: boolean) => void; +}; + +type MultiSelectProps = Readonly<{ + rootElement: HTMLElement; + options: TValue[]; + setSelectedValues: ( + setValues: (currentValues: TValue[]) => TValue[] + ) => void; + currentValues: TValue[] | undefined; + renderValue: (val: TValue) => string; + key?: (val: TValue) => TKey; + leadingChildren?: HTMLElement[] +}>; + +function uniqueBy(arr: T[], key: (val: T) => TKey): T[] { + return [...new Map(arr.map((item) => [key(item), item])).values()]; +} + +export function initMultiselectElement({ + rootElement: _rootElement, + options, + setSelectedValues, + currentValues, + renderValue, + key, + leadingChildren = [] +}: MultiSelectProps): void { + leadingChildren ??= []; + const root = _rootElement as FiberElement; + const equals = (a: TValue, b: TValue) => (key ? key(a) == key(b) : a == b) + + const toggleValue = (val: TValue, curValues: TValue[]) => + curValues.some((el) => equals(val, el)) + ? curValues.filter((el) => !equals(val, el)) + : [...curValues, val] + + _rootElement.style.maxHeight = "60vh"; + _rootElement.style.overflow = "auto"; + + for el of leadingChildren + root.appendChild el; + + function getOptionElements() { + return [..._rootElement.children] + .slice(leadingChildren.length) as MultiSelectValueElement[]; + } + + const optionElements = options + .map((optionValue) => { + const element = document.createElement( + "label" + ) as MultiSelectValueElement; + + const check = element.appendChild( + document.createElement("input") + ||> .type = "checkbox" + ); + + const labelTextSpan = element.appendChild( + document.createElement("span") + ||> .textContent = (renderValue(optionValue) ?? "unknown") + ); + + element._fibermc_optionValue = optionValue; + element._fibermc_setChecked = (checked: boolean) => + (check.checked = checked); + + //@ts-expect-error + element._fibermc_onSelect = (e) => { + let newValues: Array; + setSelectedValues((curValues) => { + newValues = toggleValue(optionValue, curValues); + if (key) { + newValues = uniqueBy(newValues, key); + } + return newValues; + }); + getOptionElements().forEach((el) => { + const isSelected = newValues.some((val) => + equals(val, el._fibermc_optionValue) + ); + el._fibermc_setChecked(isSelected); + }); + console.log(e); + }; + check.addEventListener("change", element._fibermc_onSelect); + + if (currentValues?.some((val) => equals(val, optionValue))) { + element._fibermc_setChecked(true); + } + + element.classList.add("button"); + return element; + }); + + for el of optionElements + root.appendChild el; +} + +export function MultiSelect({ + rootElement: _rootElement, + options, + setSelectedValues, + currentValues, +}: MultiSelectProps) { + const rootElement = _rootElement as FiberElement; + if (!rootElement._fibermc_initialized) { + rootElement._fibermc_initialized = true; + } +} diff --git a/public/ts/multiselect.ts b/public/ts/multiselect.ts index be86e0e..abe6575 100644 --- a/public/ts/multiselect.ts +++ b/public/ts/multiselect.ts @@ -31,41 +31,48 @@ export function initMultiselectElement({ currentValues, renderValue, key, - leadingChildren -}: MultiSelectProps) { + leadingChildren = [] +}: MultiSelectProps): void { + leadingChildren ??= []; const root = _rootElement as FiberElement; - const equals = (a: TValue, b: TValue) => (key ? key(a) == key(b) : a == b); + const equals = (a: TValue, b: TValue) => (key ? key(a) == key(b) : a == b) - const toggleValue = (val: TValue, curValues: TValue[]) => - curValues.some((el) => equals(val, el)) + const toggleValue = (val: TValue, curValues: TValue[]) => { + return curValues.some((el) => equals(val, el)) ? curValues.filter((el) => !equals(val, el)) - : [...curValues, val]; + : [...curValues, val] + } _rootElement.style.maxHeight = "60vh"; _rootElement.style.overflow = "auto"; - leadingChildren?.forEach(el => root.appendChild(el)); - const getOptionElements = () => [ - ..._rootElement.children, - ].slice(leadingChildren?.length ?? 0) as MultiSelectValueElement[]; + for (const el of leadingChildren) { + root.appendChild(el); + } - options + function getOptionElements() { + return [..._rootElement.children] + .slice(leadingChildren.length) as MultiSelectValueElement[]; + } + + const optionElements = options .map((optionValue) => { const element = document.createElement( "label" ) as MultiSelectValueElement; - const check = document.createElement("input"); - check.type = "checkbox"; - element.appendChild(check); + let ref;const check = element.appendChild( + ((ref = document.createElement("input")).type = "checkbox",ref) + ); - const labelTextSpan = document.createElement("span"); - labelTextSpan.textContent = renderValue(optionValue) ?? "unknown"; - element.appendChild(labelTextSpan); + let ref1;const labelTextSpan = element.appendChild( + ((ref1 = document.createElement("span")).textContent = (renderValue(optionValue) ?? "unknown"),ref1) + ); element._fibermc_optionValue = optionValue; - element._fibermc_setChecked = (checked: boolean) => + element._fibermc_setChecked = (checked: boolean) => { (check.checked = checked); + } //@ts-expect-error element._fibermc_onSelect = (e) => { @@ -78,8 +85,9 @@ export function initMultiselectElement({ return newValues; }); getOptionElements().forEach((el) => { - const isSelected = newValues.some((val) => - equals(val, el._fibermc_optionValue) + const isSelected = newValues.some((val) => { + return equals(val, el._fibermc_optionValue) + } ); el._fibermc_setChecked(isSelected); }); @@ -93,8 +101,11 @@ export function initMultiselectElement({ element.classList.add("button"); return element; - }) - .forEach((el) => root.appendChild(el)); + }); + + for (const el of optionElements) { + root.appendChild(el); + } } export function MultiSelect({ @@ -106,5 +117,5 @@ export function MultiSelect({ const rootElement = _rootElement as FiberElement; if (!rootElement._fibermc_initialized) { rootElement._fibermc_initialized = true; - } + };return } diff --git a/tsconfig.json b/tsconfig.json index 9dfb646..52bea77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, - "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "ES2022" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, // "lib": [], /* Specify library files to be included in the compilation. */ "allowJs": false /* Allow javascript files to be compiled. */, "checkJs": false /* Report errors in .js files. */, From cfad0b93998e42a988a67ab0659409e22c231570 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sat, 1 Mar 2025 15:15:31 -0500 Subject: [PATCH 03/11] add rsbuild configuration --- package.json | 10 +- pnpm-lock.yaml | 214 ++++++++++++++++++++++++++++++++++++++++++ rsbuild.config.ts | 11 +++ rsbuild.template.html | 166 ++++++++++++++++++++++++++++++++ tsconfig.json | 16 ++-- 5 files changed, 408 insertions(+), 9 deletions(-) create mode 100644 rsbuild.config.ts create mode 100644 rsbuild.template.html diff --git a/package.json b/package.json index c8fd15e..c86611d 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,18 @@ "test": "echo \"Error: no test specified\" && exit 1", "civet": "civet -c public/ts/*.civet -o .ts", "build": "tsc", - "__build:civet": "tsc && civet --js -c public/ts/*.civet -o ./public/ts-compiled/.js" + "__build:civet": "tsc && civet --js -c public/ts/*.civet -o ./public/ts-compiled/.js", + "dev": "rsbuild dev" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@danielx/civet": "^0.9.7" + "@danielx/civet": "^0.9.7", + "@rsbuild/core": "^1.2.14", + "typescript": "^5.8.2" + }, + "dependencies": { + "@electric-sql/pglite": "^0.2.17" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f901b5b..d71a496 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,10 +7,20 @@ settings: importers: .: + dependencies: + '@electric-sql/pglite': + specifier: ^0.2.17 + version: 0.2.17 devDependencies: '@danielx/civet': specifier: ^0.9.7 version: 0.9.7(typescript@5.8.2) + '@rsbuild/core': + specifier: ^1.2.14 + version: 1.2.14 + typescript: + specifier: ^5.8.2 + version: 5.8.2 packages: @@ -29,6 +39,9 @@ packages: yaml: optional: true + '@electric-sql/pglite@0.2.17': + resolution: {integrity: sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -39,6 +52,93 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@module-federation/error-codes@0.8.4': + resolution: {integrity: sha512-55LYmrDdKb4jt+qr8qE8U3al62ZANp3FhfVaNPOaAmdTh0jHdD8M3yf5HKFlr5xVkVO4eV/F/J2NCfpbh+pEXQ==} + + '@module-federation/runtime-tools@0.8.4': + resolution: {integrity: sha512-fjVOsItJ1u5YY6E9FnS56UDwZgqEQUrWFnouRiPtK123LUuqUI9FH4redZoKWlE1PB0ir1Z3tnqy8eFYzPO38Q==} + + '@module-federation/runtime@0.8.4': + resolution: {integrity: sha512-yZeZ7z2Rx4gv/0E97oLTF3V6N25vglmwXGgoeju/W2YjsFvWzVtCDI7zRRb0mJhU6+jmSM8jP1DeQGbea/AiZQ==} + + '@module-federation/sdk@0.8.4': + resolution: {integrity: sha512-waABomIjg/5m1rPDBWYG4KUhS5r7OUUY7S+avpaVIY/tkPWB3ibRDKy2dNLLAMaLKq0u+B1qIdEp4NIWkqhqpg==} + + '@module-federation/webpack-bundler-runtime@0.8.4': + resolution: {integrity: sha512-HggROJhvHPUX7uqBD/XlajGygMNM1DG0+4OAkk8MBQe4a18QzrRNzZt6XQbRTSG4OaEoyRWhQHvYD3Yps405tQ==} + + '@rsbuild/core@1.2.14': + resolution: {integrity: sha512-G8AqvCHBhs8Yt7pOQuS5YYjdYozp436ohaLr1iAMB/Jw01VNFh4u1tpShudRnw3NPmHhJ82wBaEu1zNaJ0VsKg==} + engines: {node: '>=16.7.0'} + hasBin: true + + '@rspack/binding-darwin-arm64@1.2.5': + resolution: {integrity: sha512-ou0NXMLp6RxY9Bx8P9lA8ArVjz/WAI/gSu5kKrdKKtMs6WKutl4vvP9A4HHZnISd9Tn00dlvDwNeNSUR7fjoDQ==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.2.5': + resolution: {integrity: sha512-RdvH9YongQlDE9+T2Xh5D2+dyiLHx2Gz38Af1uObyBRNWjF1qbuR51hOas0f2NFUdyA03j1+HWZCbE7yZrmI3w==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.2.5': + resolution: {integrity: sha512-jznk/CI/wN93fr8I1j3la/CAiGf8aG7ZHIpRBtT4CkNze0c5BcF3AaJVSBHVNQqgSv0qddxMt3SADpzV8rWZ6g==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.2.5': + resolution: {integrity: sha512-oYzcaJ0xjb1fWbbtPmjjPXeehExEgwJ8fEGYQ5TikB+p9oCLkAghnNjsz9evUhgjByxi+NTZ1YmUNwxRuQDY1Q==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.2.5': + resolution: {integrity: sha512-dzEKs8oi86Vi+TFRCPpgmfF5ANL0VmlZN45e1An7HipeI2C5B1xrz/H8V43vPy8XEvQuMmkXO6Sp82A0zlHvIA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.2.5': + resolution: {integrity: sha512-4ENeVPVSD97rRRGr6kJSm4sIPf1tKJ8vlr9hJi4sSvF7eMLWipSwIVmqRXJ2riVMRjYD2einmJ9KzI8rqQ2OwA==} + cpu: [x64] + os: [linux] + + '@rspack/binding-win32-arm64-msvc@1.2.5': + resolution: {integrity: sha512-WUoJvX/z43MWeW1JKAQIxdvqH02oLzbaGMCzIikvniZnakQovYLPH6tCYh7qD3p7uQsm+IafFddhFxTtogC3pg==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.2.5': + resolution: {integrity: sha512-YzPvmt/gpiacE6aAacz4dxgEbNWwoKYPaT4WYy/oITobnAui++iCFXC4IICSmlpoA1y7O8K3Qb9jbaB/lLhbwA==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.2.5': + resolution: {integrity: sha512-QDDshfteMZiglllm7WUh/ITemFNuexwn1Yul7cHBFGQu6HqtqKNAR0kGR8J3e15MPMlinSaygVpfRE4A0KPmjQ==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.2.5': + resolution: {integrity: sha512-q9vQmGDFZyFVMULwOFL7488WNSgn4ue94R/njDLMMIPF4K0oEJP2QT02elfG4KVGv2CbP63D7vEFN4ZNreo/Rw==} + + '@rspack/core@1.2.5': + resolution: {integrity: sha512-x/riOl05gOVGgGQFimBqS5i8XbUpBxPIKUC+tDX4hmNNkzxRaGpspZfNtcL+1HBMyYuoM6fOWGyCp2R290Uy6g==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@rspack/tracing': ^1.x + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@rspack/tracing': + optional: true + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.0.1': + resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} + engines: {node: '>=16.0.0'} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@typescript/vfs@1.6.1': resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} peerDependencies: @@ -49,6 +149,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + caniuse-lite@1.0.30001701: + resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} + + core-js@3.40.0: + resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -58,9 +164,20 @@ packages: supports-color: optional: true + isomorphic-rslog@0.0.6: + resolution: {integrity: sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A==} + engines: {node: '>=14.17.6'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -88,6 +205,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@electric-sql/pglite@0.2.17': {} + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.0': {} @@ -97,6 +216,91 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@module-federation/error-codes@0.8.4': {} + + '@module-federation/runtime-tools@0.8.4': + dependencies: + '@module-federation/runtime': 0.8.4 + '@module-federation/webpack-bundler-runtime': 0.8.4 + + '@module-federation/runtime@0.8.4': + dependencies: + '@module-federation/error-codes': 0.8.4 + '@module-federation/sdk': 0.8.4 + + '@module-federation/sdk@0.8.4': + dependencies: + isomorphic-rslog: 0.0.6 + + '@module-federation/webpack-bundler-runtime@0.8.4': + dependencies: + '@module-federation/runtime': 0.8.4 + '@module-federation/sdk': 0.8.4 + + '@rsbuild/core@1.2.14': + dependencies: + '@rspack/core': 1.2.5(@swc/helpers@0.5.15) + '@rspack/lite-tapable': 1.0.1 + '@swc/helpers': 0.5.15 + core-js: 3.40.0 + jiti: 2.4.2 + transitivePeerDependencies: + - '@rspack/tracing' + + '@rspack/binding-darwin-arm64@1.2.5': + optional: true + + '@rspack/binding-darwin-x64@1.2.5': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.2.5': + optional: true + + '@rspack/binding-linux-arm64-musl@1.2.5': + optional: true + + '@rspack/binding-linux-x64-gnu@1.2.5': + optional: true + + '@rspack/binding-linux-x64-musl@1.2.5': + optional: true + + '@rspack/binding-win32-arm64-msvc@1.2.5': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.2.5': + optional: true + + '@rspack/binding-win32-x64-msvc@1.2.5': + optional: true + + '@rspack/binding@1.2.5': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.2.5 + '@rspack/binding-darwin-x64': 1.2.5 + '@rspack/binding-linux-arm64-gnu': 1.2.5 + '@rspack/binding-linux-arm64-musl': 1.2.5 + '@rspack/binding-linux-x64-gnu': 1.2.5 + '@rspack/binding-linux-x64-musl': 1.2.5 + '@rspack/binding-win32-arm64-msvc': 1.2.5 + '@rspack/binding-win32-ia32-msvc': 1.2.5 + '@rspack/binding-win32-x64-msvc': 1.2.5 + + '@rspack/core@1.2.5(@swc/helpers@0.5.15)': + dependencies: + '@module-federation/runtime-tools': 0.8.4 + '@rspack/binding': 1.2.5 + '@rspack/lite-tapable': 1.0.1 + caniuse-lite: 1.0.30001701 + optionalDependencies: + '@swc/helpers': 0.5.15 + + '@rspack/lite-tapable@1.0.1': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@typescript/vfs@1.6.1(typescript@5.8.2)': dependencies: debug: 4.4.0 @@ -106,12 +310,22 @@ snapshots: acorn@8.14.0: {} + caniuse-lite@1.0.30001701: {} + + core-js@3.40.0: {} + debug@4.4.0: dependencies: ms: 2.1.3 + isomorphic-rslog@0.0.6: {} + + jiti@2.4.2: {} + ms@2.1.3: {} + tslib@2.8.1: {} + typescript@5.8.2: {} unplugin@2.2.0: diff --git a/rsbuild.config.ts b/rsbuild.config.ts new file mode 100644 index 0000000..4acf846 --- /dev/null +++ b/rsbuild.config.ts @@ -0,0 +1,11 @@ +export default { + html: { + template: "./rsbuild.template.html", + }, + source: { + entry: { + resource_loader: './public/ts/resource_loader.ts', + list_search: './public/ts/list_search.ts', + }, + }, + }; \ No newline at end of file diff --git a/rsbuild.template.html b/rsbuild.template.html new file mode 100644 index 0000000..64559d9 --- /dev/null +++ b/rsbuild.template.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + FiberMC - Minecraft Fabric Mod List + + + +
+ +
+
+
+
+
+ + +
+ Sort +
+ + + + + +
+
+ +
+ Version +
+
+
+ + + Table Viewtable_view +
+ + +
+ Message of the Day! + + +
+
+
+ + +
+ +
+ + + + + + + + +
+ + + diff --git a/tsconfig.json b/tsconfig.json index 52bea77..dfcdd01 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,13 @@ // "incremental": true, /* Enable incremental compilation */ "target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "module": "ES2022" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ + "lib": [ + "ESNext", + "DOM" + // "DOM.AsyncIterable", + // "DOM.Iterable" + ] /* Specify library files to be included in the compilation. */, + "skipLibCheck": true, "allowJs": false /* Allow javascript files to be compiled. */, "checkJs": false /* Report errors in .js files. */, // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ @@ -21,7 +27,6 @@ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ @@ -31,13 +36,11 @@ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ @@ -49,13 +52,11 @@ "esModuleInterop": false /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ @@ -63,5 +64,6 @@ "noUnusedParameters": false, "rootDir": "./public/ts", "outDir": "./public/js/ts-compiled" - } + }, + "exclude": ["node_modules"] } From 8ea06d1ce600934396b1ec072cd11a51cd6b1240 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sat, 1 Mar 2025 16:40:30 -0500 Subject: [PATCH 04/11] experimenting with local postgres --- public/ts/list_elem_default.ts | 17 ++- public/ts/list_search.ts | 32 +++-- public/ts/mod_search_logic.ts | 243 +++++++++++++++++++++++++++++---- public/ts/mod_search_table.ts | 10 +- public/ts/resource_loader.ts | 53 ++++--- 5 files changed, 290 insertions(+), 65 deletions(-) diff --git a/public/ts/list_elem_default.ts b/public/ts/list_elem_default.ts index 077999c..d051838 100644 --- a/public/ts/list_elem_default.ts +++ b/public/ts/list_elem_default.ts @@ -113,11 +113,20 @@ class DefaultListElementRendererImpl extends ListElementRenderer< fillAuthorDiv(authorDiv, modData); - for (const category of modData.categories) { - // if not "Fabric" - if (category !== fabric_category_id) { - categories.appendChild(getElementForCategory(category)()); + try { + for (const category of modData.categories) { + // if not "Fabric" + if (category !== fabric_category_id) { + categories.appendChild(getElementForCategory(category)()); + } } + + } catch (err) { + console.group(); + console.warn("Failed to fill mod categories info."); + console.warn(err); + console.warn(modData); + console.groupEnd(); } desc.textContent = modData.summary; diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index be5ded1..9a3beb2 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -6,11 +6,11 @@ import { batch_containers, init, initSearch, - loader, + registerOnLoad, resultsListElement, setLiHeight, setResultsListElement, -} from "./mod_search_logic.js"; +} from "./mod_search_logic"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; @@ -62,14 +62,13 @@ function createBatch(batchIdx: number, data_batches: Mod[][]): void { // } // } // }); +const preInitializationCallbacks: (() => void)[] = []; -loader.addCompletionFunc(() => { - setResultsListElement(getElementById("search_results_list")); -} -); +preInitializationCallbacks.push( + () => setResultsListElement(getElementById("search_results_list"))) var modCategoryElements: (() => Node)[]; -loader.addCompletionFunc(() => { +registerOnLoad(() => { modCategoryElements = CATEGORIES .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) .map((c) => () => c.cloneNode(true)); @@ -123,14 +122,17 @@ executeIfWhenDOMContentLoaded(() => { ); }); -loader.addCompletionFunc(() => { - initSearch({ - results_persist: true, - batchCreationFunc: createBatch, - lazyLoadBatches: true, - batch_size: 20, - li_height: modeLiHeights[currentViewIdx](), - }); +preInitializationCallbacks.push +initSearch({ + results_persist: true, + batchCreationFunc: createBatch, + lazyLoadBatches: true, + batch_size: 20, + li_height: modeLiHeights[currentViewIdx], + preInitializationCallbacks +}); + +registerOnLoad(() => { updateViewModes(); }); diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 2a8095f..4e53258 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -1,5 +1,5 @@ import { setHidden, getElementById } from "./util.js"; -import { AsyncDataResourceLoader } from "./resource_loader.js"; +import { AsyncDataResourceLoader } from "./resource_loader"; import { getSortFunc, getSortState, @@ -8,13 +8,24 @@ import { } from "./table_sort.js"; import { BaseMod, Mod, baseModToMod, versionOrd } from "./mod_types.js"; import { initMultiselectElement } from "./multiselect.js"; +import { PGlite } from "@electric-sql/pglite"; + +let prevTime = performance.now(); +function logtime(message: string) { + console.log("PERF: " + message, performance.now() - prevTime); + prevTime = performance.now(); +} + +logtime("start file") +const db = new PGlite("idb://fibermc"); +await db.waitReady +logtime("Database Ready!") export { init, initSearch, initCategoriesSidebar, fabric_category_id, - loader, mod_data, setModData, CATEGORIES, @@ -33,6 +44,7 @@ export { LI_HEIGHT, BATCH_SIZE, setLiHeight, + registerOnLoad, }; type CategoryElement = HTMLButtonElement & { @@ -51,27 +63,185 @@ type Category = { modCount: number; filteredModCount: number | null; }; +console.log("PROOF OF ALIVE"); //============== // DATA LOADING //============== console.log("hostname", window.location.hostname); const apiUrl = `https://${ - window.location.hostname === "localhost" - ? "localhost:5001" - : window.location.hostname - // "dev.fibermc.com" + // window.location.hostname === "localhost" + // ? "localhost:5001" + // : window.location.hostname + "dev.fibermc.com" }/api/v1.0`; + + +var localLoader = new AsyncDataResourceLoader({ + completionWaitForDCL: true, +}).addResourceFn(async () => { + logtime("Start local db query") + if (!db) { + return undefined; + } + console.log("HAVE DB"); + // const res = await db.query("SELECT COUNT(*) FROM mod_data;"); + // // @ts-ignore + // console.log("CHECK DB RES", res, res.rows[0].count); + + // @ts-ignore + // if (res.rows[0].count) { + console.log("HAVE ROWS"); + + const temp_mod_data = await db.query( + `SELECT + id, + name, + mr_slug, + cf_slug, + summary, + categories, + authors, + date_released as "dateReleased", + date_modified as "dateModified", + download_count as "downloadCount", + mc_versions, + s_name, + s_latest_mc_version as "s_latestMCVersion" + FROM mod_data ORDER BY download_count DESC LIMIT 100; + ` + ); + logtime("Start local db query...done!") + return temp_mod_data.rows as Mod[]; + // } +}, [ + async (temp_mod_data) => { + if (!temp_mod_data) { + return; + } + console.log("SET ROWS", temp_mod_data); + + setModData(temp_mod_data); + mod_data.sort((a, b) => b.downloadCount - a.downloadCount); + }, +]) +.addResource(`${apiUrl}/Categories`, [ + (jsonData) => { + categoryNames = jsonData; + console.log("categoryNames", categoryNames); + }, +]) +.addCompletionFunc(initCategoriesSidebar); + +configureModsLoader(localLoader); + // Load mod data from external file var loader = new AsyncDataResourceLoader({ completionWaitForDCL: true, }) .addResource(`${apiUrl}/Mods`, [ - (jsonData) => { + async (jsonData) => { setModData(jsonData.map(baseModToMod)); // Sort descending mod_data.sort((a, b) => b.downloadCount - a.downloadCount); + logtime("api data loaded") console.log("mod_data", mod_data); + await db.exec(` +CREATE TABLE IF NOT EXISTS mod_data ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + mr_slug TEXT, + cf_slug TEXT, + summary TEXT, + categories TEXT[], -- Using array type for categories + authors JSONB, -- Using JSONB for authors array + date_released TIMESTAMP, + date_modified TIMESTAMP, + download_count BIGINT, + mc_versions TEXT[], -- Using array type for versions + s_name TEXT, + s_latest_mc_version NUMERIC, + s_date_modified BIGINT, + latest_mc_version TEXT, + s_author TEXT +); + `); + console.log("TABLE CREATED IF NEEDED"); + // First create the table if it doesn't exist + await db.exec(` + CREATE TABLE IF NOT EXISTS mod_data ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + mr_slug TEXT, + cf_slug TEXT, + summary TEXT, + categories TEXT[], + authors JSONB, + date_released TIMESTAMP, + date_modified TIMESTAMP, + download_count BIGINT, + mc_versions TEXT[], + s_name TEXT, + s_latest_mc_version NUMERIC, + s_date_modified BIGINT, + latest_mc_version TEXT, + s_author TEXT + ); + `); + + // Now insert or update each mod individually with parameterized queries + for (const mod of mod_data){//.slice(0, 5)) { + console.log("INSERTING MOD", mod); + await db.query( + ` + INSERT INTO mod_data ( + id, name, mr_slug, cf_slug, summary, categories, authors, + date_released, date_modified, download_count, mc_versions, + s_name, s_latest_mc_version, s_date_modified, latest_mc_version, s_author + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, $11, + $12, $13, $14, $15, $16 + ) + ON CONFLICT (id) + DO UPDATE SET + name = $2, + mr_slug = $3, + cf_slug = $4, + summary = $5, + categories = $6, + authors = $7, + date_released = $8, + date_modified = $9, + download_count = $10, + mc_versions = $11, + s_name = $12, + s_latest_mc_version = $13, + s_date_modified = $14, + latest_mc_version = $15, + s_author = $16 + `, + [ + mod.id, + mod.name, + mod.mr_slug, + mod.cf_slug, + mod.summary, + mod.categories, // This assumes the library can handle array parameters + JSON.stringify(mod.authors), // Convert authors array to JSON string + mod.dateReleased, + mod.dateModified, + mod.downloadCount, + mod.mc_versions, // This assumes the library can handle array parameters + mod.s_name, + mod.s_latestMCVersion, + mod.s_dateModified, + mod.latestMCVersion, + mod.s_author, + ] + ); + } }, ]) .addResource(`${apiUrl}/Categories`, [ @@ -81,10 +251,21 @@ var loader = new AsyncDataResourceLoader({ }, ]) .addCompletionFunc(initCategoriesSidebar); +configureModsLoader(loader); + var timestamp: string; var currentSelectedVersions: [string, number][] = []; -function init() { + +function registerOnLoad(fn: () => void): void { + localLoader.addCompletionFunc(fn); + loader.addCompletionFunc(fn); +} + +function configureModsLoader(loader: AsyncDataResourceLoader): void { loader + .addCompletionFunc(() => { + initSearchInternal(); + }) .addCompletionFunc(() => { defaultSearchInput.value = getUrlSearchValue() ?? ""; searchTextChanged(getUrlSearchValue()); @@ -202,8 +383,12 @@ function init() { versions ); }); - }) - .fetchResources(); + }); +} + +function init() { + localLoader.fetchResources(); + loader.fetchResources(); } function formatDate(date: string | number | Date) { @@ -822,14 +1007,25 @@ function setLiHeight(liHeight: number) { var defaultSearchInput: HTMLInputElement; type InitSearchOptions = { results_persist: boolean; - li_height?: number; + li_height?: () => number; batch_size?: number; listElemCreationFunc?: (modData: Mod) => HTMLElement; batchCreationFunc: BatchCreationFunc; listCreationFunc?: ListBuilderFunc; lazyLoadBatches?: (() => void) | boolean; + /** + * pre-initialization callbacks, because order matters + */ + preInitializationCallbacks: (() => void)[] }; +var GLOBAL_SEARCH_OPTIONS: InitSearchOptions; function initSearch(options: InitSearchOptions) { + GLOBAL_SEARCH_OPTIONS = options; +} +function initSearchInternal() { + const options = GLOBAL_SEARCH_OPTIONS; + options.preInitializationCallbacks.forEach(fn => fn()) + results_persist = options.results_persist; const defaultOptions = { results_persist: false, @@ -841,7 +1037,7 @@ function initSearch(options: InitSearchOptions) { lazyLoadBatches: true, }; - LI_HEIGHT = options.li_height ?? defaultOptions.li_height; + LI_HEIGHT = options.li_height?.() ?? defaultOptions.li_height; BATCH_SIZE = options.batch_size ?? defaultOptions.batch_size; function resultsViewBuilder(options: InitSearchOptions) { if (options.listElemCreationFunc) { @@ -1010,29 +1206,30 @@ const storeBatches = ( useContainers = true ) => { const endIdx = startIdx + batchSize; - const data_batch: Mod[] = []; + + const data_batch: Mod[] = results.slice( + startIdx, + Math.min(endIdx, results.length) + ); + data_batches.push(data_batch); + const nextBatchSize = Math.min(batchSize, results.length - endIdx); - for (let i = startIdx; i < endIdx; i++) { - data_batch.push(results[i]); - } + if (useContainers) { const batch_container = document.createElement("div"); batch_container.setAttribute("class", "item_batch"); - // batch_container.style.height = LI_HEIGHT*batchSize+'px'; - // batch_container.style.minHeight = LI_HEIGHT*batchSize+'px'; batch_containers.push(batch_container); resultsListElement.appendChild(batch_container); if (nextBatchSize <= 0) { - const height = computeLiHeightPx(LI_HEIGHT, data_batch.length); - batch_container.style.height = height + "px"; - batch_container.style.minHeight = height + "px"; + const heightStyle = + computeLiHeightPx(LI_HEIGHT, data_batch.length) + "px"; + batch_container.style.height = heightStyle; + batch_container.style.minHeight = heightStyle; } } - data_batches.push(data_batch); - if (nextBatchSize > 0) storeBatches(results, endIdx, nextBatchSize, useContainers); }; diff --git a/public/ts/mod_search_table.ts b/public/ts/mod_search_table.ts index e765171..6a2fb1f 100644 --- a/public/ts/mod_search_table.ts +++ b/public/ts/mod_search_table.ts @@ -4,7 +4,6 @@ import { initSearch, initCategoriesSidebar, fabric_category_id, - loader, mod_data, setModData, CATEGORIES, @@ -17,7 +16,8 @@ import { pxAboveTop, pxBelowBottom, data_batches as dataBatches, -} from "./mod_search_logic.js"; + registerOnLoad, +} from "./mod_search_logic"; import { getElementById } from "./util.js"; import { Mod, @@ -208,7 +208,7 @@ type ModWithElem = Mod & { elem: HTMLTableRowElement; }; -loader.addCompletionFunc(() => { +registerOnLoad(() => { loadbar = createLoadbar({ parentElement: getElementById("content_main"), hideOnComplete: true, @@ -228,10 +228,10 @@ loader.addCompletionFunc(() => { } } }); -loader.addCompletionFunc(() => +registerOnLoad(() => setResultsListElement(getElementById("search_results_content")) ); -loader.addCompletionFunc(() => +registerOnLoad(() => initSearch({ results_persist: true, listElemCreationFunc: createListElement, diff --git a/public/ts/resource_loader.ts b/public/ts/resource_loader.ts index 825e41a..c897b67 100644 --- a/public/ts/resource_loader.ts +++ b/public/ts/resource_loader.ts @@ -3,22 +3,26 @@ export { AsyncDataResourceLoader, ResourceEntry }; import { executeIfWhenDOMContentLoaded, FunctionBatch } from "./util.js"; class ResourceEntry { - url: string; + fn: () => Promise; funcs: FunctionBatch; /** * - * @param {string} resourceURL + * @param {string} resourceFetchFn * @param {Array} responseFuncs */ constructor( - resourceURL: string, + resourceFetchFn: () => Promise, responseFuncs: ((response: TResponse) => void)[] ) { - this.url = resourceURL; + this.fn = resourceFetchFn; this.funcs = new FunctionBatch(responseFuncs); } } +const requestOpts: RequestInit = { + method: "GET", +}; + class AsyncDataResourceLoader { resources: ResourceEntry[]; completionWaitForDCL: boolean; @@ -61,7 +65,31 @@ class AsyncDataResourceLoader { resourceURL: string, responseFuncs: ((response: TResponse) => void)[] ) { - this.resources.push(new ResourceEntry(resourceURL, responseFuncs)); + this.resources.push( + new ResourceEntry( + () => + fetch(new Request(resourceURL, requestOpts)).then( + (response) => { + if (response.status === 200) { + return response.json(); + } else { + throw new Error( + "Requested data file could not be retrieved from server." + ); + } + } + ), + responseFuncs + ) + ); + return this; + } + + addResourceFn( + resourceFn: () => Promise, + responseFuncs: ((response: TResponse) => void)[] + ) { + this.resources.push(new ResourceEntry(resourceFn, responseFuncs)) return this; } @@ -76,22 +104,11 @@ class AsyncDataResourceLoader { fetchResources() { const promises = []; - const requestOpts: RequestInit = { - method: "GET", - }; for (const resource of this.resources) { promises.push( - fetch(new Request(resource.url, requestOpts)) - .then((response) => { - if (response.status === 200) { - return response.json(); - } else { - throw new Error( - "Requested data file could not be retrieved from server." - ); - } - }) + resource + .fn() .then((resJson) => { console.debug(resJson); resource.funcs.runAll(resJson); From ac69735d677bb7f5a982b1d2059ce1fb14028ad6 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sat, 1 Mar 2025 23:38:29 -0500 Subject: [PATCH 05/11] batching writes and tracking perf --- public/ts/list_search.civet | 29 ++-- public/ts/list_search.ts | 14 +- public/ts/mod_search_logic.ts | 247 ++++++++++++++++++---------------- 3 files changed, 154 insertions(+), 136 deletions(-) diff --git a/public/ts/list_search.civet b/public/ts/list_search.civet index 2054bf0..8221f4c 100644 --- a/public/ts/list_search.civet +++ b/public/ts/list_search.civet @@ -6,13 +6,14 @@ import { batch_containers, init, initSearch, - loader, + registerOnLoad, resultsListElement, setLiHeight, setResultsListElement, } from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; +import {IdempotentComponent} from "./idempotent_component" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -63,12 +64,13 @@ function createBatch(batchIdx: number, data_batches: Mod[][]): void { // } // }); -loader.addCompletionFunc(() => - setResultsListElement(getElementById("search_results_list")); -); +const preInitializationCallbacks: (() => void)[] = []; + +preInitializationCallbacks.push( + () => setResultsListElement(getElementById("search_results_list"))) var modCategoryElements: (() => Node)[]; -loader.addCompletionFunc(() => { +preInitializationCallbacks.push(() => { modCategoryElements = CATEGORIES .map((c) => document.createElement("li") ||> .textContent = c.name) .map((c) => () => c.cloneNode(true)); @@ -122,14 +124,15 @@ executeIfWhenDOMContentLoaded(() => { ); }); -loader.addCompletionFunc(() => { - initSearch({ - results_persist: true, - batchCreationFunc: createBatch, - lazyLoadBatches: true, - batch_size: 20, - li_height: modeLiHeights[currentViewIdx](), - }); +initSearch({ + results_persist: true, + batchCreationFunc: createBatch, + lazyLoadBatches: true, + batch_size: 20, + li_height: modeLiHeights[currentViewIdx], + preInitializationCallbacks +}); +registerOnLoad(() => { updateViewModes(); }); diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index 9a3beb2..a14e87a 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -10,9 +10,10 @@ import { resultsListElement, setLiHeight, setResultsListElement, -} from "./mod_search_logic"; +} from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; +import {IdempotentComponent} from "./idempotent_component" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -62,13 +63,14 @@ function createBatch(batchIdx: number, data_batches: Mod[][]): void { // } // } // }); + const preInitializationCallbacks: (() => void)[] = []; preInitializationCallbacks.push( () => setResultsListElement(getElementById("search_results_list"))) var modCategoryElements: (() => Node)[]; -registerOnLoad(() => { +preInitializationCallbacks.push(() => { modCategoryElements = CATEGORIES .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) .map((c) => () => c.cloneNode(true)); @@ -122,7 +124,6 @@ executeIfWhenDOMContentLoaded(() => { ); }); -preInitializationCallbacks.push initSearch({ results_persist: true, batchCreationFunc: createBatch, @@ -131,9 +132,8 @@ initSearch({ li_height: modeLiHeights[currentViewIdx], preInitializationCallbacks }); - -registerOnLoad(() => { - updateViewModes(); -}); +registerOnLoad(() =>( { + updateViewModes() {; } +})); init(); diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 4e53258..1dd29ad 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -16,10 +16,10 @@ function logtime(message: string) { prevTime = performance.now(); } -logtime("start file") +logtime("start file"); const db = new PGlite("idb://fibermc"); -await db.waitReady -logtime("Database Ready!") +await db.waitReady; +logtime("Database Ready!"); export { init, @@ -76,21 +76,21 @@ const apiUrl = `https://${ "dev.fibermc.com" }/api/v1.0`; - var localLoader = new AsyncDataResourceLoader({ completionWaitForDCL: true, -}).addResourceFn(async () => { - logtime("Start local db query") - if (!db) { - return undefined; - } - console.log("HAVE DB"); - // const res = await db.query("SELECT COUNT(*) FROM mod_data;"); - // // @ts-ignore - // console.log("CHECK DB RES", res, res.rows[0].count); +}) + .addResourceFn(async () => { + logtime("Start local db query"); + if (!db) { + return undefined; + } + console.log("HAVE DB"); + // const res = await db.query("SELECT COUNT(*) FROM mod_data;"); + // // @ts-ignore + // console.log("CHECK DB RES", res, res.rows[0].count); - // @ts-ignore - // if (res.rows[0].count) { + // @ts-ignore + // if (res.rows[0].count) { console.log("HAVE ROWS"); const temp_mod_data = await db.query( @@ -111,27 +111,27 @@ var localLoader = new AsyncDataResourceLoader({ FROM mod_data ORDER BY download_count DESC LIMIT 100; ` ); - logtime("Start local db query...done!") + logtime("Start local db query...done!"); return temp_mod_data.rows as Mod[]; - // } -}, [ - async (temp_mod_data) => { - if (!temp_mod_data) { - return; - } - console.log("SET ROWS", temp_mod_data); - - setModData(temp_mod_data); - mod_data.sort((a, b) => b.downloadCount - a.downloadCount); - }, -]) -.addResource(`${apiUrl}/Categories`, [ - (jsonData) => { - categoryNames = jsonData; - console.log("categoryNames", categoryNames); - }, -]) -.addCompletionFunc(initCategoriesSidebar); + // } + }, [ + async (temp_mod_data) => { + if (!temp_mod_data) { + return; + } + console.log("SET ROWS", temp_mod_data); + + setModData(temp_mod_data); + mod_data.sort((a, b) => b.downloadCount - a.downloadCount); + }, + ]) + .addResource(`${apiUrl}/Categories`, [ + (jsonData) => { + categoryNames = jsonData; + console.log("categoryNames", categoryNames); + }, + ]) + .addCompletionFunc(initCategoriesSidebar); configureModsLoader(localLoader); @@ -144,29 +144,12 @@ var loader = new AsyncDataResourceLoader({ setModData(jsonData.map(baseModToMod)); // Sort descending mod_data.sort((a, b) => b.downloadCount - a.downloadCount); - logtime("api data loaded") + logtime("api data loaded"); console.log("mod_data", mod_data); - await db.exec(` -CREATE TABLE IF NOT EXISTS mod_data ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - mr_slug TEXT, - cf_slug TEXT, - summary TEXT, - categories TEXT[], -- Using array type for categories - authors JSONB, -- Using JSONB for authors array - date_released TIMESTAMP, - date_modified TIMESTAMP, - download_count BIGINT, - mc_versions TEXT[], -- Using array type for versions - s_name TEXT, - s_latest_mc_version NUMERIC, - s_date_modified BIGINT, - latest_mc_version TEXT, - s_author TEXT -); - `); console.log("TABLE CREATED IF NEEDED"); + + //-- DROP TABLE IF EXISTS mod_data; + // // First create the table if it doesn't exist await db.exec(` CREATE TABLE IF NOT EXISTS mod_data ( @@ -175,73 +158,100 @@ CREATE TABLE IF NOT EXISTS mod_data ( mr_slug TEXT, cf_slug TEXT, summary TEXT, - categories TEXT[], - authors JSONB, + categories INT4[], -- Text array for categories + authors JSONB, -- JSONB for authors date_released TIMESTAMP, date_modified TIMESTAMP, download_count BIGINT, - mc_versions TEXT[], + mc_versions TEXT[], -- Text array for versions s_name TEXT, s_latest_mc_version NUMERIC, s_date_modified BIGINT, latest_mc_version TEXT, s_author TEXT ); - `); - - // Now insert or update each mod individually with parameterized queries - for (const mod of mod_data){//.slice(0, 5)) { - console.log("INSERTING MOD", mod); - await db.query( - ` - INSERT INTO mod_data ( - id, name, mr_slug, cf_slug, summary, categories, authors, - date_released, date_modified, download_count, mc_versions, - s_name, s_latest_mc_version, s_date_modified, latest_mc_version, s_author - ) - VALUES ( - $1, $2, $3, $4, $5, $6, $7, - $8, $9, $10, $11, - $12, $13, $14, $15, $16 - ) - ON CONFLICT (id) - DO UPDATE SET - name = $2, - mr_slug = $3, - cf_slug = $4, - summary = $5, - categories = $6, - authors = $7, - date_released = $8, - date_modified = $9, - download_count = $10, - mc_versions = $11, - s_name = $12, - s_latest_mc_version = $13, - s_date_modified = $14, - latest_mc_version = $15, - s_author = $16 - `, - [ - mod.id, - mod.name, - mod.mr_slug, - mod.cf_slug, - mod.summary, - mod.categories, // This assumes the library can handle array parameters - JSON.stringify(mod.authors), // Convert authors array to JSON string - mod.dateReleased, - mod.dateModified, - mod.downloadCount, - mod.mc_versions, // This assumes the library can handle array parameters - mod.s_name, - mod.s_latestMCVersion, - mod.s_dateModified, - mod.latestMCVersion, - mod.s_author, - ] - ); - } + `); + + // Batch processing using PGlite's transaction feature + await db.transaction(async (tx) => { + // Process in batches to avoid memory issues + const BATCH_SIZE = 500; + + for (let i = 0; i < mod_data.length; i += BATCH_SIZE) { + const batch = mod_data.slice(i, i + BATCH_SIZE); + console.log( + `Processing batch ${ + Math.floor(i / BATCH_SIZE) + 1 + } of ${Math.ceil(mod_data.length / BATCH_SIZE)}` + ); + + // Create a large multi-value INSERT statement + let valuesSql = []; + let params = []; + let paramIndex = 1; + + for (const mod of batch) { + // Add placeholders for this row + valuesSql.push(`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++}::INT4[], $${paramIndex++}::jsonb, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++}::text[], $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++})`); + + // Add values to params array + params.push( + mod.id, + mod.name, + mod.mr_slug, + mod.cf_slug, + mod.summary, + mod.categories, + JSON.stringify(mod.authors), + mod.dateReleased, + mod.dateModified, + mod.downloadCount, + mod.mc_versions, + mod.s_name, + mod.s_latestMCVersion, + mod.s_dateModified, + mod.latestMCVersion, + mod.s_author + ); + } + + // Build the complete query with all batch values + const query = ` + INSERT INTO mod_data ( + id, name, mr_slug, cf_slug, summary, categories, authors, + date_released, date_modified, download_count, mc_versions, + s_name, s_latest_mc_version, s_date_modified, latest_mc_version, s_author + ) + VALUES ${valuesSql.join(",")} + ON CONFLICT (id) + DO UPDATE SET + name = EXCLUDED.name, + mr_slug = EXCLUDED.mr_slug, + cf_slug = EXCLUDED.cf_slug, + summary = EXCLUDED.summary, + categories = EXCLUDED.categories, + authors = EXCLUDED.authors, + date_released = EXCLUDED.date_released, + date_modified = EXCLUDED.date_modified, + download_count = EXCLUDED.download_count, + mc_versions = EXCLUDED.mc_versions, + s_name = EXCLUDED.s_name, + s_latest_mc_version = EXCLUDED.s_latest_mc_version, + s_date_modified = EXCLUDED.s_date_modified, + latest_mc_version = EXCLUDED.latest_mc_version, + s_author = EXCLUDED.s_author + `; + + await tx.query(query, params); + } + }); + console.log( + "DONE SETUP", + await db.query("SELECT * FROM mod_data;") + ); }, ]) .addResource(`${apiUrl}/Categories`, [ @@ -263,6 +273,11 @@ function registerOnLoad(fn: () => void): void { function configureModsLoader(loader: AsyncDataResourceLoader): void { loader + .addCompletionFunc(() => { + GLOBAL_SEARCH_OPTIONS.preInitializationCallbacks.forEach((fn) => + fn() + ); + }) .addCompletionFunc(() => { initSearchInternal(); }) @@ -1016,7 +1031,7 @@ type InitSearchOptions = { /** * pre-initialization callbacks, because order matters */ - preInitializationCallbacks: (() => void)[] + preInitializationCallbacks: (() => void)[]; }; var GLOBAL_SEARCH_OPTIONS: InitSearchOptions; function initSearch(options: InitSearchOptions) { @@ -1024,7 +1039,7 @@ function initSearch(options: InitSearchOptions) { } function initSearchInternal() { const options = GLOBAL_SEARCH_OPTIONS; - options.preInitializationCallbacks.forEach(fn => fn()) + // options.preInitializationCallbacks.forEach((fn) => fn()); results_persist = options.results_persist; const defaultOptions = { From 91ea39a4549bb378658b73d03635cb5e17017132 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 11:02:57 -0500 Subject: [PATCH 06/11] switch to signals for much state management --- package.json | 3 +- pnpm-lock.yaml | 8 + public/ts/effect.ts | 39 ++++ public/ts/initCategoriesSidebar.ts | 281 ++++++++++++++++++++++++++++ public/ts/list_elem_default.ts | 2 +- public/ts/list_elem_detailed.ts | 5 +- public/ts/list_search.civet | 16 +- public/ts/list_search.ts | 21 ++- public/ts/mod_search_logic.ts | 291 +++-------------------------- public/ts/mod_search_table.ts | 35 ++-- public/ts/search_page_state.ts | 7 + 11 files changed, 406 insertions(+), 302 deletions(-) create mode 100644 public/ts/effect.ts create mode 100644 public/ts/initCategoriesSidebar.ts create mode 100644 public/ts/search_page_state.ts diff --git a/package.json b/package.json index c86611d..234c3fa 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "typescript": "^5.8.2" }, "dependencies": { - "@electric-sql/pglite": "^0.2.17" + "@electric-sql/pglite": "^0.2.17", + "signal-polyfill": "^0.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d71a496..860ff02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@electric-sql/pglite': specifier: ^0.2.17 version: 0.2.17 + signal-polyfill: + specifier: ^0.2.2 + version: 0.2.2 devDependencies: '@danielx/civet': specifier: ^0.9.7 @@ -175,6 +178,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + signal-polyfill@0.2.2: + resolution: {integrity: sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -324,6 +330,8 @@ snapshots: ms@2.1.3: {} + signal-polyfill@0.2.2: {} + tslib@2.8.1: {} typescript@5.8.2: {} diff --git a/public/ts/effect.ts b/public/ts/effect.ts new file mode 100644 index 0000000..09745cf --- /dev/null +++ b/public/ts/effect.ts @@ -0,0 +1,39 @@ +import { Signal } from "signal-polyfill"; + +let needsEnqueue = true; + +const w = new Signal.subtle.Watcher(() => { + if (needsEnqueue) { + needsEnqueue = false; + queueMicrotask(processPending); + } +}); + +function processPending() { + needsEnqueue = true; + + for (const s of w.getPending()) { + s.get(); + } + + w.watch(); +} + +export function effect(callback: (() => void) | (() => (() => void))) { + let cleanup: (() => void) | undefined; + + const computed = new Signal.Computed(() => { + typeof cleanup === "function" && cleanup(); + // @ts-expect-error its OK for an assignment of void to undefined + cleanup = callback(); + }); + + w.watch(computed); + computed.get(); + + return () => { + w.unwatch(computed); + typeof cleanup === "function" && cleanup(); + cleanup = undefined; + }; +} \ No newline at end of file diff --git a/public/ts/initCategoriesSidebar.ts b/public/ts/initCategoriesSidebar.ts new file mode 100644 index 0000000..ff52833 --- /dev/null +++ b/public/ts/initCategoriesSidebar.ts @@ -0,0 +1,281 @@ +import { effect } from "./effect"; +import { + updateUrlFromSearchOptions, + getSearchOptionsFromState, + searchTextChanged, + selectCategories, + getSearchOptionsFromUrl, +} from "./mod_search_logic"; +import { Mod } from "./mod_types"; +import { Signal } from "signal-polyfill"; +import { CATEGORY_NAMES, MOD_DATA, TOTAL_MOD_COUNT } from "./search_page_state"; + +type CategoryElement = HTMLButtonElement & { + bool_mode: number | undefined; + cat_id: number; + selected: boolean | undefined; +}; + +const isCategoryElement = (el: any): el is CategoryElement => + el.cat_id !== undefined; + +type Category = { + htmlElement: CategoryElement; + name: string; +}; + +type CategoryWithCounts = Category & { + renderCount: () => void; + modCount: number; + filteredModCount: number | null; +} + + +var categories_sidebar_elem: HTMLElement; +let initialized = false; +export function initCategoriesSidebar() { + if (initialized) { + return; + } + initialized = true; + const categories = CATEGORIES.get(); + //TODO Group "Selected" items? + //TODO "select multiple" toggle + //TODO Option to sort categories by name or by num mods in category + //TODO Display "searching in these categories" under searchbar. With option to click them to remove. + const getCategoriesSidebarElem = () => { + const elem = document.getElementById("categories_list"); + if (!elem) { + throw new Error( + "Could not find 'categories_sidebar_elem' (Element Id: 'categories_list')" + ); + } + return elem; + }; + categories_sidebar_elem = getCategoriesSidebarElem(); + + const createAllModsElement = () => { + const elem = document.createElement("button") as CategoryElement; + + elem.classList.add("reset_button"); + elem.cat_id = -1; + const title = "All mods (reset)"; + + elem.textContent = title + " "; + const mod_count = document.createElement("span"); + mod_count.textContent = MOD_DATA.get().length.toFixed(0); + elem.appendChild(mod_count); + elem.addEventListener("click", clearFilters); + categories_sidebar_elem.appendChild(elem); + + elem.classList.add("reset_categories_button"); + + effect(() => { + console.log("MODS COUNT EFFECT", MOD_DATA.get().length, + TOTAL_MOD_COUNT.get() +) + mod_count.textContent = buildCategoryCountStr( + MOD_DATA.get().length, + TOTAL_MOD_COUNT.get() + ); + }) + }; + createAllModsElement(); + + { + // Init CATEGORIES + + effect(() => { + const categories = CATEGORIES.get(); + for (const category of categories) { + category.modCount = 0; + } + // Mod Counts + for (const mod of MOD_DATA.get()) { + for (const cat_id of mod.categories) { + categories[cat_id].modCount += 1; + } + } + updateCategoryModCounts(MOD_DATA.get()); + }) + } + + for (let i = 0; i < categories.length; i++) { + if (categories[i].name.toUpperCase() === "FABRIC") { + fabric_category_id = i; + break; + } + } + // TODO Restructure this, jfc + for (let i = 0; i < categories.length; i++) {} + const sorted_CATEGORIES = categories.slice().sort(function (a, b) { + return b.modCount - a.modCount; + }); + for (let i = 0; i < sorted_CATEGORIES.length; i++) { + const category = sorted_CATEGORIES[i]; + if (category.modCount === 0) { + continue; + } + const cat_elem = category.htmlElement; + cat_elem.selected = false; + applyCategorySelection(cat_elem); + cat_elem.addEventListener("click", onClick); + categories_sidebar_elem.appendChild(cat_elem); + } + + effect(() => { + // sort the category elements by mod count + MOD_DATA.get(); // needed to force this to react to that signal + const sorted_CATEGORIES = CATEGORIES.get().slice().sort(function (a, b) { + return b.modCount - a.modCount; + }); + console.log("REORDER", sorted_CATEGORIES) + for (let i = 0; i < sorted_CATEGORIES.length; i++) { + // appending an item that already exists in the dom moves it + categories_sidebar_elem.append(sorted_CATEGORIES[i].htmlElement) + } + }) + + // 0=none, 1=AND, 2=NOT | OR?? + const NUM_BOOL_OPS = 2; + function onClick(e: Event) { + const cat_elem = e.target; + if (!isCategoryElement(cat_elem)) { + throw new Error( + "Category click listener was applied to an element without CategoryElement metadata." + ); + } + const bool_mode = cat_elem.bool_mode ?? BoolMode.None; + cat_elem.bool_mode = + bool_mode < NUM_BOOL_OPS ? bool_mode + 1 : BoolMode.None; + applyCategorySelection(cat_elem); + updateUrlFromSearchOptions(getSearchOptionsFromState()); + searchTextChanged(undefined, true); + } + function clearFilters() { + for (const cat of categories) { + const cat_elem = cat.htmlElement; + cat_elem.classList.remove("and"); + cat_elem.classList.remove("not"); + cat_elem.bool_mode = BoolMode.None; + } + applyCategorySelections(); + updateUrlFromSearchOptions(getSearchOptionsFromState()); + searchTextChanged(undefined, true); + } + selectCategories(getSearchOptionsFromUrl()); +} +export enum BoolMode { + None = 0, + And = 1, + Not = 2, +} +export var fabric_category_id: number; +export function applyCategorySelection(cat_elem: CategoryElement) { + if (cat_elem.bool_mode == BoolMode.And) { + cat_elem.classList.add("and"); + } else { + cat_elem.classList.remove("and"); //.border = '2px solid var(--color-element-1)'; + } + if (cat_elem.bool_mode == BoolMode.Not) { + cat_elem.classList.add("not"); + } else { + cat_elem.classList.remove("not"); //.border = '2px solid var(--color-element-1)'; + } +} + +export function applyCategorySelections() { + effect(() => { + CATEGORIES.get() + .map((cat) => cat.htmlElement) + .forEach(applyCategorySelection); + }) +} + +function buildCategoryCountStr( + totalModCount: number, + filteredModCount: number | null +) { + return filteredModCount !== null + ? `${filteredModCount} / ${totalModCount}` + : totalModCount.toString(); +}; + +export function getSelectedCategoryIds() { + const selected_cat_ids: { + and: number[]; + not: number[]; + } = { + and: [], + not: [], + }; + + for (const category of CATEGORIES.get()) { + const cat_elem = category.htmlElement; + if (cat_elem.bool_mode == 1) { + selected_cat_ids.and.push(cat_elem.cat_id); + } else if (cat_elem.bool_mode == 2) { + selected_cat_ids.not.push(cat_elem.cat_id); + } + } + return selected_cat_ids; +} + +export function updateCategoryModCounts(mods: Mod[]) { + const categories = CATEGORIES.get(); + const selectedCategories = getSelectedCategoryIds(); + const isFiltering = + MOD_DATA.get().length !== mods.length || + selectedCategories.and.length > 0 || + selectedCategories.not.length > 0; + if (isFiltering) { + for (const category of categories) { + category.filteredModCount = 0; + } + // Mod Counts + for (const mod of mods) { + for (const cat_id of mod.categories) { + categories[cat_id].filteredModCount! += 1; + } + } + } else { + for (const category of categories) { + category.filteredModCount = null; + } + } + + TOTAL_MOD_COUNT.set(isFiltering ? mods.length : null); + for (const category of categories) { + category.renderCount(); + } +} + + +const createCategoryElement = (categoryId: number): CategoryElement => { + const cat_elem = document.createElement("button") as CategoryElement; + cat_elem.classList.add("reset_button"); + cat_elem.cat_id = categoryId; //category.categoryId; + return cat_elem; +}; + +export const CATEGORIES = new Signal.Computed(() => + CATEGORY_NAMES.get().map((name, idx) => { + const categoryElement = createCategoryElement(idx); + const countElement = document.createElement("span"); + categoryElement.textContent = name + " "; + categoryElement.appendChild(countElement); + + return { + name: name, + modCount: 0, + filteredModCount: null, + renderCount() { + countElement.textContent = buildCategoryCountStr( + this.modCount, + this.filteredModCount + ); + }, + htmlElement: categoryElement, + }; + }) +); diff --git a/public/ts/list_elem_default.ts b/public/ts/list_elem_default.ts index d051838..49310e0 100644 --- a/public/ts/list_elem_default.ts +++ b/public/ts/list_elem_default.ts @@ -1,6 +1,6 @@ import { ListElementRenderer } from "./list_elem_renderer.js"; import { fillAuthorDiv, getElementForCategory } from "./list_item_shared.js"; -import { fabric_category_id } from "./mod_search_logic.js"; +import { fabric_category_id } from "./initCategoriesSidebar.js"; import { Mod } from "./mod_types.js"; import { formatNumberCompact } from "./number_formatter.js"; import { diff --git a/public/ts/list_elem_detailed.ts b/public/ts/list_elem_detailed.ts index 6d4b0e5..4feaabf 100644 --- a/public/ts/list_elem_detailed.ts +++ b/public/ts/list_elem_detailed.ts @@ -1,12 +1,13 @@ import { ListElementRenderer } from "./list_elem_renderer.js"; import { fillAuthorDiv, getElementForCategory } from "./list_item_shared.js"; -import { CATEGORIES, fabric_category_id } from "./mod_search_logic.js"; +import { fabric_category_id } from "./initCategoriesSidebar.js"; import { Mod } from "./mod_types.js"; import { formatNumberCompact } from "./number_formatter.js"; import { createCurseLinkIcon, createModrinthLinkIcon, } from "./platform_links.js"; +import { CATEGORIES } from "./initCategoriesSidebar.js" type ListElementTemplate = { li: HTMLLIElement; @@ -135,7 +136,7 @@ class DetailedListElementRendererImpl extends ListElementRenderer< // if not "Fabric" if (category !== fabric_category_id) { const catElem = document.createElement("li"); - catElem.textContent = CATEGORIES[category].name; + catElem.textContent = CATEGORIES.get()[category].name; categories.appendChild(catElem); } } diff --git a/public/ts/list_search.civet b/public/ts/list_search.civet index 8221f4c..911b6eb 100644 --- a/public/ts/list_search.civet +++ b/public/ts/list_search.civet @@ -2,7 +2,6 @@ import { DefaultListElementRenderer } from "./list_elem_default.js"; import { DetailedListElementRenderer } from "./list_elem_detailed.js"; import { setModCategoryElements } from "./list_item_shared.js"; import { - CATEGORIES, batch_containers, init, initSearch, @@ -13,7 +12,8 @@ import { } from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; -import {IdempotentComponent} from "./idempotent_component" +import { CATEGORIES } from "./initCategoriesSidebar" +import { effect } from "./effect" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -71,11 +71,15 @@ preInitializationCallbacks.push( var modCategoryElements: (() => Node)[]; preInitializationCallbacks.push(() => { - modCategoryElements = CATEGORIES - .map((c) => document.createElement("li") ||> .textContent = c.name) - .map((c) => () => c.cloneNode(true)); + console.log("CREATE CATEGORY ELEMS", CATEGORIES.get()) + effect(() => { + modCategoryElements = CATEGORIES.get() + .map((c) => document.createElement("li") ||> .textContent = c.name) + .map((c) => () => c.cloneNode(true)); - setModCategoryElements(modCategoryElements); + setModCategoryElements(modCategoryElements); + + }) }); function getLiHeight() { diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index a14e87a..c0b9347 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -2,7 +2,6 @@ import { DefaultListElementRenderer } from "./list_elem_default.js"; import { DetailedListElementRenderer } from "./list_elem_detailed.js"; import { setModCategoryElements } from "./list_item_shared.js"; import { - CATEGORIES, batch_containers, init, initSearch, @@ -13,7 +12,9 @@ import { } from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; -import {IdempotentComponent} from "./idempotent_component" +import { IdempotentComponent } from "./idempotent_component" +import { CATEGORIES } from "./initCategoriesSidebar" +import { effect } from "./effect" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -70,13 +71,17 @@ preInitializationCallbacks.push( () => setResultsListElement(getElementById("search_results_list"))) var modCategoryElements: (() => Node)[]; -preInitializationCallbacks.push(() => { - modCategoryElements = CATEGORIES - .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) - .map((c) => () => c.cloneNode(true)); +preInitializationCallbacks.push(() =>( { + log: console.log("CREATE CATEGORY ELEMS", CATEGORIES.get()), + effect: effect(() => { + modCategoryElements = CATEGORIES.get() + .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) + .map((c) => () => c.cloneNode(true)); - setModCategoryElements(modCategoryElements); -}); + setModCategoryElements(modCategoryElements); + + }) +})); function getLiHeight() { const fmt = (val: string) => val.slice(0, val.length - 2) diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 1dd29ad..2f2b256 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -8,7 +8,10 @@ import { } from "./table_sort.js"; import { BaseMod, Mod, baseModToMod, versionOrd } from "./mod_types.js"; import { initMultiselectElement } from "./multiselect.js"; +import { CATEGORY_NAMES, MOD_DATA, TOTAL_MOD_COUNT } from "./search_page_state" import { PGlite } from "@electric-sql/pglite"; +import { effect } from "./effect.js"; +import { CATEGORIES, initCategoriesSidebar, BoolMode, applyCategorySelections, getSelectedCategoryIds, updateCategoryModCounts } from "./initCategoriesSidebar.js"; let prevTime = performance.now(); function logtime(message: string) { @@ -24,12 +27,8 @@ logtime("Database Ready!"); export { init, initSearch, - initCategoriesSidebar, - fabric_category_id, mod_data, setModData, - CATEGORIES, - setCategories, resultsListElement, setResultsListElement, storeBatches, @@ -47,22 +46,6 @@ export { registerOnLoad, }; -type CategoryElement = HTMLButtonElement & { - bool_mode: number | undefined; - cat_id: number; - selected: boolean | undefined; -}; - -const isCategoryElement = (el: any): el is CategoryElement => - el.cat_id !== undefined; - -type Category = { - htmlElement: CategoryElement; - renderCount: () => void; - name: string; - modCount: number; - filteredModCount: number | null; -}; console.log("PROOF OF ALIVE"); //============== @@ -127,11 +110,15 @@ var localLoader = new AsyncDataResourceLoader({ ]) .addResource(`${apiUrl}/Categories`, [ (jsonData) => { - categoryNames = jsonData; - console.log("categoryNames", categoryNames); + CATEGORY_NAMES.set(jsonData); + console.log("categoryNames", jsonData); }, ]) - .addCompletionFunc(initCategoriesSidebar); + .addCompletionFunc(() => { + effect(() => { + initCategoriesSidebar(); + }) + }); configureModsLoader(localLoader); @@ -256,11 +243,15 @@ var loader = new AsyncDataResourceLoader({ ]) .addResource(`${apiUrl}/Categories`, [ (jsonData) => { - categoryNames = jsonData; - console.log("categoryNames", categoryNames); + CATEGORY_NAMES.set(jsonData); + console.log("categoryNames", jsonData); }, ]) - .addCompletionFunc(initCategoriesSidebar); + .addCompletionFunc(() => { + effect(() => { + initCategoriesSidebar(); + }) + }); configureModsLoader(loader); var timestamp: string; @@ -422,36 +413,13 @@ function updateTimestamp(timestamp: Date) { var mod_data: Mod[]; function setModData(n_mod_data: Mod[]) { mod_data = n_mod_data; -} - -var categoryNames: string[]; -var CATEGORIES: Category[]; -function setCategories(n_categories: Category[]) { - CATEGORIES = n_categories; + MOD_DATA.set(n_mod_data) } //==================== // Filter Search Data //==================== -function getSelectedCategoryIds() { - const selected_cat_ids: { - and: number[]; - not: number[]; - } = { - and: [], - not: [], - }; - for (const category of CATEGORIES) { - const cat_elem = category.htmlElement; - if (cat_elem.bool_mode == 1) { - selected_cat_ids.and.push(cat_elem.cat_id); - } else if (cat_elem.bool_mode == 2) { - selected_cat_ids.not.push(cat_elem.cat_id); - } - } - return selected_cat_ids; -} // Apply filter to search data (based on user selections) function getFilteredList() { const selected_cat_ids = getSelectedCategoryIds(); @@ -475,212 +443,6 @@ function getFilteredList() { } return search_objs; } -var fabric_category_id: number; -var categories_sidebar_elem: HTMLElement; -function applyCategorySelection(cat_elem: CategoryElement) { - if (cat_elem.bool_mode == BoolMode.And) { - cat_elem.classList.add("and"); - } else { - cat_elem.classList.remove("and"); //.border = '2px solid var(--color-element-1)'; - } - if (cat_elem.bool_mode == BoolMode.Not) { - cat_elem.classList.add("not"); - } else { - cat_elem.classList.remove("not"); //.border = '2px solid var(--color-element-1)'; - } -} - -function applyCategorySelections() { - CATEGORIES.map((cat) => cat.htmlElement).forEach(applyCategorySelection); -} - -const buildCategoryCountStr = ( - totalModCount: number, - filteredModCount: number | null -) => { - return filteredModCount !== null - ? `${filteredModCount} / ${totalModCount}` - : totalModCount.toString(); -}; -var setTotalModCount: (count: number | null) => void; - -function initCategoryModCounts(mods: Mod[]) { - for (const category of CATEGORIES) { - category.modCount = 0; - } - // Mod Counts - for (const mod of mods) { - for (const cat_id of mod.categories) { - CATEGORIES[cat_id].modCount += 1; - } - } - updateCategoryModCounts(mod_data); -} - -function updateCategoryModCounts(mods: Mod[]) { - const selectedCategories = getSelectedCategoryIds(); - const isFiltering = - mod_data.length !== mods.length || - selectedCategories.and.length > 0 || - selectedCategories.not.length > 0; - if (isFiltering) { - for (const category of CATEGORIES) { - category.filteredModCount = 0; - } - // Mod Counts - for (const mod of mods) { - for (const cat_id of mod.categories) { - CATEGORIES[cat_id].filteredModCount! += 1; - } - } - } else { - for (const category of CATEGORIES) { - category.filteredModCount = null; - } - } - - setTotalModCount(isFiltering ? mods.length : null); - for (const category of CATEGORIES) { - category.renderCount(); - } -} - -function initCategoriesSidebar() { - //TODO Group "Selected" items? - //TODO "select multiple" toggle - //TODO Option to sort categories by name or by num mods in category - //TODO Display "searching in these categories" under searchbar. With option to click them to remove. - - const getCategoriesSidebarElem = () => { - const elem = document.getElementById("categories_list"); - if (!elem) { - throw new Error( - "Could not find 'categories_sidebar_elem' (Element Id: 'categories_list')" - ); - } - return elem; - }; - categories_sidebar_elem = getCategoriesSidebarElem(); - - const createAllModsElement = () => { - const elem = document.createElement("button") as CategoryElement; - - elem.classList.add("reset_button"); - elem.cat_id = -1; - const title = "All mods (reset)"; - - elem.textContent = title + " "; - const mod_count = document.createElement("span"); - mod_count.textContent = mod_data.length.toFixed(0); - elem.appendChild(mod_count); - elem.addEventListener("click", clearFilters); - categories_sidebar_elem.appendChild(elem); - - elem.classList.add("reset_categories_button"); - - return { - setTotalModCount: (count: number | null) => { - mod_count.textContent = buildCategoryCountStr( - mod_data.length, - count - ); - }, - }; - }; - const allModsElementRet = createAllModsElement(); - setTotalModCount = allModsElementRet.setTotalModCount; - - const createCategoryElement = (categoryId: number): CategoryElement => { - const cat_elem = document.createElement("button") as CategoryElement; - cat_elem.classList.add("reset_button"); - cat_elem.cat_id = categoryId; //category.categoryId; - return cat_elem; - }; - - { - // Init CATEGORIES - setCategories( - categoryNames.map((name, idx) => { - const categoryElement = createCategoryElement(idx); - const countElement = document.createElement("span"); - categoryElement.textContent = name + " "; - categoryElement.appendChild(countElement); - - return { - name: name, - modCount: 0, - filteredModCount: null, - renderCount() { - countElement.textContent = buildCategoryCountStr( - this.modCount, - this.filteredModCount - ); - }, - htmlElement: categoryElement, - }; - }) - ); - initCategoryModCounts(mod_data); - } - - for (let i = 0; i < CATEGORIES.length; i++) { - if (CATEGORIES[i].name.toUpperCase() === "FABRIC") { - fabric_category_id = i; - break; - } - } - // TODO Restructure this, jfc - for (let i = 0; i < CATEGORIES.length; i++) {} - const sorted_CATEGORIES = CATEGORIES.slice().sort(function (a, b) { - return b.modCount - a.modCount; - }); - for (let i = 0; i < sorted_CATEGORIES.length; i++) { - const category = sorted_CATEGORIES[i]; - if (category.modCount === 0) { - continue; - } - const cat_elem = category.htmlElement; - cat_elem.selected = false; - applyCategorySelection(cat_elem); - cat_elem.addEventListener("click", onClick); - categories_sidebar_elem.appendChild(cat_elem); - } - - // 0=none, 1=AND, 2=NOT | OR?? - const NUM_BOOL_OPS = 2; - function onClick(e: Event) { - const cat_elem = e.target; - if (!isCategoryElement(cat_elem)) { - throw new Error( - "Category click listener was applied to an element without CategoryElement metadata." - ); - } - const bool_mode = cat_elem.bool_mode ?? BoolMode.None; - cat_elem.bool_mode = - bool_mode < NUM_BOOL_OPS ? bool_mode + 1 : BoolMode.None; - applyCategorySelection(cat_elem); - updateUrlFromSearchOptions(getSearchOptionsFromState()); - searchTextChanged(undefined, true); - } - function clearFilters() { - for (const cat of CATEGORIES) { - const cat_elem = cat.htmlElement; - cat_elem.classList.remove("and"); - cat_elem.classList.remove("not"); - cat_elem.bool_mode = BoolMode.None; - } - applyCategorySelections(); - updateUrlFromSearchOptions(getSearchOptionsFromState()); - searchTextChanged(undefined, true); - } - selectCategories(getSearchOptionsFromUrl()); -} - -enum BoolMode { - None = 0, - And = 1, - Not = 2, -} //============== // Search Logic @@ -700,8 +462,8 @@ export type SearchOptions = Readonly<{ // var searchOptions: SearchOptions; -function getSearchOptionsFromState(): SearchOptions { - const categories = CATEGORIES.map((cat) => ({ +export function getSearchOptionsFromState(): SearchOptions { + const categories = CATEGORIES.get().map((cat) => ({ name: cat.name, bool_mode: cat.htmlElement.bool_mode, })); @@ -730,7 +492,7 @@ const urlFormatCategories = (categories: string[]) => const urlDecodeCategories = (urlEncString: string | undefined | null) => urlEncString ? urlEncString.split(encodeURIComponent(",")) : undefined; -function updateUrlFromSearchOptions(options: SearchOptions) { +export function updateUrlFromSearchOptions(options: SearchOptions) { if ("URLSearchParams" in window) { var searchParams = new URLSearchParams(window.location.search); @@ -799,7 +561,7 @@ function getUrlSearchValue(): string | undefined { return searchParams.get("search") ?? undefined; } -function getSearchOptionsFromUrl(): SearchOptions { +export function getSearchOptionsFromUrl(): SearchOptions { var searchParams = new URLSearchParams(window.location.search); return { search: searchParams.get("search") ?? undefined, @@ -818,16 +580,17 @@ function getSearchOptionsFromUrl(): SearchOptions { }; } -function selectCategories({ +export function selectCategories({ categoryIncludes, categoryExcludes, }: SearchOptions): void { + const categories = CATEGORIES.get(); categoryIncludes?.forEach((element) => { - CATEGORIES.find((cat) => cat.name === element)!.htmlElement.bool_mode = + categories.find((cat) => cat.name === element)!.htmlElement.bool_mode = BoolMode.And; }); categoryExcludes?.forEach((element) => { - CATEGORIES.find((cat) => cat.name === element)!.htmlElement.bool_mode = + categories.find((cat) => cat.name === element)!.htmlElement.bool_mode = BoolMode.Not; }); applyCategorySelections(); @@ -915,7 +678,7 @@ const filterByVersion = (results: Mod[]) => { //================ // Input Handling //================ -function searchTextChanged(value?: string, resultsPersist?: boolean) { +export function searchTextChanged(value?: string, resultsPersist?: boolean) { const search_objects = getFilteredList(); const searchValue = value ?? defaultSearchInput.value; diff --git a/public/ts/mod_search_table.ts b/public/ts/mod_search_table.ts index 6a2fb1f..26ad1ba 100644 --- a/public/ts/mod_search_table.ts +++ b/public/ts/mod_search_table.ts @@ -2,22 +2,17 @@ import { createLoadbar, LoadbarResult } from "./loadbar.js"; import { init, initSearch, - initCategoriesSidebar, - fabric_category_id, mod_data, - setModData, - CATEGORIES, - setCategories, resultsListElement, setResultsListElement, storeBatches, runBatches, resetBatches, - pxAboveTop, pxBelowBottom, data_batches as dataBatches, registerOnLoad, } from "./mod_search_logic"; +import { CATEGORIES, fabric_category_id } from "./initCategoriesSidebar.js"; import { getElementById } from "./util.js"; import { Mod, @@ -72,11 +67,12 @@ function createListElement(modData: Mod) { const categories = document.createElement("td"); categories.setAttribute("class", "item_categories"); + const categoriesData = CATEGORIES.get(); for (const category of modData.categories) { // if not "Fabric" if (category !== fabric_category_id) { const catElem = document.createElement("li"); - catElem.textContent = CATEGORIES[category].name; + catElem.textContent = categoriesData[category].name; categories.appendChild(catElem); } } @@ -228,18 +224,17 @@ registerOnLoad(() => { } } }); -registerOnLoad(() => - setResultsListElement(getElementById("search_results_content")) -); -registerOnLoad(() => - initSearch({ - results_persist: true, - listElemCreationFunc: createListElement, - batchCreationFunc: createBatch, - listCreationFunc: buildTableBatched, - lazyLoadBatches: lazyLoadBatches, - batch_size: BATCH_SIZE, - }) -); + +initSearch({ + results_persist: true, + listElemCreationFunc: createListElement, + batchCreationFunc: createBatch, + listCreationFunc: buildTableBatched, + lazyLoadBatches: lazyLoadBatches, + batch_size: BATCH_SIZE, + preInitializationCallbacks: [ + () => setResultsListElement(getElementById("search_results_content")) + ] +}) init(); diff --git a/public/ts/search_page_state.ts b/public/ts/search_page_state.ts new file mode 100644 index 0000000..1f80717 --- /dev/null +++ b/public/ts/search_page_state.ts @@ -0,0 +1,7 @@ +import { Signal } from "signal-polyfill"; +import { Mod } from "./mod_types"; + +export const MOD_DATA = new Signal.State([]); +export const TOTAL_MOD_COUNT = new Signal.State(null); +export const CATEGORY_NAMES = new Signal.State([]); + From d14f4a9b6b2cbeb9ae475d5ee96d3a7ebfb04868 Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 15:03:12 -0500 Subject: [PATCH 07/11] further switch to signals, particularly around CATEGORIES --- public/ts/initCategoriesSidebar.ts | 343 +++++++++++++++++------------ public/ts/list_elem_detailed.ts | 4 +- public/ts/list_item_shared.ts | 16 +- public/ts/list_search.civet | 22 +- public/ts/list_search.ts | 23 +- public/ts/mod_search_logic.ts | 30 +-- public/ts/search_page_state.ts | 2 +- 7 files changed, 246 insertions(+), 194 deletions(-) diff --git a/public/ts/initCategoriesSidebar.ts b/public/ts/initCategoriesSidebar.ts index ff52833..7748e64 100644 --- a/public/ts/initCategoriesSidebar.ts +++ b/public/ts/initCategoriesSidebar.ts @@ -3,12 +3,12 @@ import { updateUrlFromSearchOptions, getSearchOptionsFromState, searchTextChanged, - selectCategories, getSearchOptionsFromUrl, + SearchOptions, } from "./mod_search_logic"; import { Mod } from "./mod_types"; import { Signal } from "signal-polyfill"; -import { CATEGORY_NAMES, MOD_DATA, TOTAL_MOD_COUNT } from "./search_page_state"; +import { MOD_DATA, TOTAL_MOD_COUNT } from "./search_page_state"; type CategoryElement = HTMLButtonElement & { bool_mode: number | undefined; @@ -19,17 +19,6 @@ type CategoryElement = HTMLButtonElement & { const isCategoryElement = (el: any): el is CategoryElement => el.cat_id !== undefined; -type Category = { - htmlElement: CategoryElement; - name: string; -}; - -type CategoryWithCounts = Category & { - renderCount: () => void; - modCount: number; - filteredModCount: number | null; -} - var categories_sidebar_elem: HTMLElement; let initialized = false; @@ -38,7 +27,7 @@ export function initCategoriesSidebar() { return; } initialized = true; - const categories = CATEGORIES.get(); + const categories = CATEGORIES.ELEMENTS; //TODO Group "Selected" items? //TODO "select multiple" toggle //TODO Option to sort categories by name or by num mods in category @@ -71,95 +60,67 @@ export function initCategoriesSidebar() { elem.classList.add("reset_categories_button"); effect(() => { - console.log("MODS COUNT EFFECT", MOD_DATA.get().length, - TOTAL_MOD_COUNT.get() -) + console.log( + "MODS COUNT EFFECT", + MOD_DATA.get().length, + TOTAL_MOD_COUNT.get() + ); mod_count.textContent = buildCategoryCountStr( MOD_DATA.get().length, TOTAL_MOD_COUNT.get() ); - }) + }); }; createAllModsElement(); { // Init CATEGORIES - effect(() => { - const categories = CATEGORIES.get(); - for (const category of categories) { - category.modCount = 0; - } // Mod Counts - for (const mod of MOD_DATA.get()) { - for (const cat_id of mod.categories) { - categories[cat_id].modCount += 1; - } + const categories = CATEGORIES.BY_ID; + const totalModCountsByCatId = countModsByCategoryId(MOD_DATA.get()); + console.log("INITIAL COUNTS", totalModCountsByCatId, MOD_DATA.get()) + for (const category of categories) { + category.setTotalModCount( + totalModCountsByCatId[category.id]?.count ?? 0 + ); } - updateCategoryModCounts(MOD_DATA.get()); - }) + + updateFilteredCategoryModCounts(MOD_DATA.get()); + }); } - for (let i = 0; i < categories.length; i++) { - if (categories[i].name.toUpperCase() === "FABRIC") { - fabric_category_id = i; - break; + effect(() => { + const categoryNames = CATEGORIES.NAMES.get(); + for (let i = 0; i < categoryNames.length; i++) { + if (categoryNames[i].toUpperCase() === "FABRIC") { + fabric_category_id = i; + break; + } } - } - // TODO Restructure this, jfc - for (let i = 0; i < categories.length; i++) {} - const sorted_CATEGORIES = categories.slice().sort(function (a, b) { - return b.modCount - a.modCount; }); - for (let i = 0; i < sorted_CATEGORIES.length; i++) { - const category = sorted_CATEGORIES[i]; - if (category.modCount === 0) { - continue; - } - const cat_elem = category.htmlElement; - cat_elem.selected = false; - applyCategorySelection(cat_elem); - cat_elem.addEventListener("click", onClick); - categories_sidebar_elem.appendChild(cat_elem); - } + effect(() => { // sort the category elements by mod count MOD_DATA.get(); // needed to force this to react to that signal - const sorted_CATEGORIES = CATEGORIES.get().slice().sort(function (a, b) { - return b.modCount - a.modCount; + const sorted_CATEGORIES = CATEGORIES.BY_ID.slice().sort(function ( + a, + b + ) { + return b.totalModCount - a.totalModCount; }); - console.log("REORDER", sorted_CATEGORIES) + console.log("REORDER", sorted_CATEGORIES); for (let i = 0; i < sorted_CATEGORIES.length; i++) { // appending an item that already exists in the dom moves it - categories_sidebar_elem.append(sorted_CATEGORIES[i].htmlElement) + categories_sidebar_elem.append(sorted_CATEGORIES[i].element); } - }) + }); - // 0=none, 1=AND, 2=NOT | OR?? - const NUM_BOOL_OPS = 2; - function onClick(e: Event) { - const cat_elem = e.target; - if (!isCategoryElement(cat_elem)) { - throw new Error( - "Category click listener was applied to an element without CategoryElement metadata." - ); - } - const bool_mode = cat_elem.bool_mode ?? BoolMode.None; - cat_elem.bool_mode = - bool_mode < NUM_BOOL_OPS ? bool_mode + 1 : BoolMode.None; - applyCategorySelection(cat_elem); - updateUrlFromSearchOptions(getSearchOptionsFromState()); - searchTextChanged(undefined, true); - } function clearFilters() { - for (const cat of categories) { - const cat_elem = cat.htmlElement; - cat_elem.classList.remove("and"); - cat_elem.classList.remove("not"); - cat_elem.bool_mode = BoolMode.None; + for (const cat of Object.values(categories)) { + cat.setBoolMode(BoolMode.None); } - applyCategorySelections(); updateUrlFromSearchOptions(getSearchOptionsFromState()); searchTextChanged(undefined, true); } @@ -171,25 +132,18 @@ export enum BoolMode { Not = 2, } export var fabric_category_id: number; -export function applyCategorySelection(cat_elem: CategoryElement) { - if (cat_elem.bool_mode == BoolMode.And) { - cat_elem.classList.add("and"); - } else { - cat_elem.classList.remove("and"); //.border = '2px solid var(--color-element-1)'; - } - if (cat_elem.bool_mode == BoolMode.Not) { - cat_elem.classList.add("not"); - } else { - cat_elem.classList.remove("not"); //.border = '2px solid var(--color-element-1)'; - } -} -export function applyCategorySelections() { - effect(() => { - CATEGORIES.get() - .map((cat) => cat.htmlElement) - .forEach(applyCategorySelection); - }) +function selectCategories({ + categoryIncludes, + categoryExcludes, +}: SearchOptions): void { + const categories = CATEGORIES.ELEMENTS; + categoryIncludes?.forEach((catName) => { + categories[catName]!.setBoolMode(BoolMode.And); + }); + categoryExcludes?.forEach((catName) => { + categories[catName]!.setBoolMode(BoolMode.Not); + }); } function buildCategoryCountStr( @@ -199,7 +153,7 @@ function buildCategoryCountStr( return filteredModCount !== null ? `${filteredModCount} / ${totalModCount}` : totalModCount.toString(); -}; +} export function getSelectedCategoryIds() { const selected_cat_ids: { @@ -210,47 +164,42 @@ export function getSelectedCategoryIds() { not: [], }; - for (const category of CATEGORIES.get()) { - const cat_elem = category.htmlElement; - if (cat_elem.bool_mode == 1) { - selected_cat_ids.and.push(cat_elem.cat_id); - } else if (cat_elem.bool_mode == 2) { - selected_cat_ids.not.push(cat_elem.cat_id); + for (const category of Object.values(CATEGORIES.ELEMENTS)) { + const bool_mode = category.boolMode; + if (bool_mode === BoolMode.And) { + selected_cat_ids.and.push(category.id); + } else if (bool_mode === BoolMode.Not) { + selected_cat_ids.not.push(category.id); } } return selected_cat_ids; } -export function updateCategoryModCounts(mods: Mod[]) { - const categories = CATEGORIES.get(); +export function updateFilteredCategoryModCounts(mods: Mod[]) { + const categories = CATEGORIES.BY_ID; const selectedCategories = getSelectedCategoryIds(); const isFiltering = MOD_DATA.get().length !== mods.length || selectedCategories.and.length > 0 || selectedCategories.not.length > 0; if (isFiltering) { - for (const category of categories) { - category.filteredModCount = 0; - } // Mod Counts - for (const mod of mods) { - for (const cat_id of mod.categories) { - categories[cat_id].filteredModCount! += 1; - } + const filteredModCountsById = countModsByCategoryId(mods); + + for (const category of categories) { + category.setFilteredModCount( + filteredModCountsById[category.id]?.count ?? null + ); } } else { for (const category of categories) { - category.filteredModCount = null; + category.setFilteredModCount(null); } } TOTAL_MOD_COUNT.set(isFiltering ? mods.length : null); - for (const category of categories) { - category.renderCount(); - } } - const createCategoryElement = (categoryId: number): CategoryElement => { const cat_elem = document.createElement("button") as CategoryElement; cat_elem.classList.add("reset_button"); @@ -258,24 +207,148 @@ const createCategoryElement = (categoryId: number): CategoryElement => { return cat_elem; }; -export const CATEGORIES = new Signal.Computed(() => - CATEGORY_NAMES.get().map((name, idx) => { - const categoryElement = createCategoryElement(idx); - const countElement = document.createElement("span"); - categoryElement.textContent = name + " "; - categoryElement.appendChild(countElement); - - return { - name: name, - modCount: 0, - filteredModCount: null, - renderCount() { - countElement.textContent = buildCategoryCountStr( - this.modCount, - this.filteredModCount +export class CategoryEl { + private _id: number; + private _name: string; + private _element: CategoryElement; + private _countElement: HTMLSpanElement; + private _totalModCount: number; + private _filteredModCount: number | null; + constructor(name: string, idx: number) { + this._id = idx; + this._name = name; + this._element = createCategoryElement(idx); + this._countElement = document.createElement("span"); + this._element.textContent = name + " "; + this._element.appendChild(this._countElement); + this._totalModCount = 0; + this._filteredModCount = null; + + this._element.selected = false; + // applyCategorySelection(cat_elem); + + // 0=none, 1=AND, 2=NOT | OR?? + const NUM_BOOL_OPS = 2; + this._element.addEventListener("click", (e: Event) => { + const cat_elem = e.target; + if (!isCategoryElement(cat_elem)) { + throw new Error( + "Category click listener was applied to an element without CategoryElement metadata." ); - }, - htmlElement: categoryElement, - }; - }) -); + } + const bool_mode = cat_elem.bool_mode ?? BoolMode.None; + this.setBoolMode( + bool_mode < NUM_BOOL_OPS ? bool_mode + 1 : BoolMode.None + ); + updateUrlFromSearchOptions(getSearchOptionsFromState()); + searchTextChanged(undefined, true); + }); + } + + private renderModCounts( + totalModCount: number, + filteredModCount: number | null + ) { + console.log("renderModCounts", totalModCount, filteredModCount) + this._countElement.textContent = buildCategoryCountStr( + totalModCount, + filteredModCount + ); + } + + get boolMode(): BoolMode { + return this._element.bool_mode ?? BoolMode.None; + } + setBoolMode(mode: BoolMode) { + this._element.bool_mode = mode; + this.applyCategorySelection(); + } + + isSelected(): boolean { + return !!this._element.selected; + } + + private applyCategorySelection() { + if (this._element.bool_mode == BoolMode.And) { + this._element.classList.add("and"); + } else { + this._element.classList.remove("and"); //.border = '2px solid var(--color-element-1)'; + } + if (this._element.bool_mode == BoolMode.Not) { + this._element.classList.add("not"); + } else { + this._element.classList.remove("not"); //.border = '2px solid var(--color-element-1)'; + } + } + + setFilteredModCount(count: number | null): void { + this._filteredModCount = count; + this.renderModCounts(this._totalModCount, this._filteredModCount); + } + + setTotalModCount(count: number): void { + this._totalModCount = count; + this.renderModCounts(this._totalModCount, this._filteredModCount); + } + + get id(): number { + this.setFilteredModCount; + return this._id; + } + + get name(): string { + return this._name; + } + + get element(): HTMLButtonElement { + return this._element; + } + + get totalModCount(): number { + return this._totalModCount; + } +} + +const _categories_by_id = new Signal.State([]); +const _categories_by_name = new Signal.State>({}); +export const CATEGORIES = { + NAMES: new Signal.State([]), + get ELEMENTS(): Record { + return _categories_by_name.get(); + }, + get BY_ID(): CategoryEl[] { + return _categories_by_id.get(); + }, + setCategoryNames: function (categoryNames: string[]) { + if (this.NAMES.get().length > 0) { + this.NAMES.set(categoryNames); + const byId = categoryNames.map( + (name, idx) => new CategoryEl(name, idx) + ); + _categories_by_id.set(byId); + _categories_by_name.set( + Object.fromEntries(byId.map((cat) => [cat.name, cat])) + ); + } + }, +}; + +effect(() => { + CATEGORIES.setCategoryNames(CATEGORIES.NAMES.get()); +}); + +function countModsByCategoryId(mods: Mod[]): Record{ + return mods.reduce( + (accum, mod) => { + for (const cat_id of mod.categories) { + let catEntry = accum[cat_id]; + if (catEntry === undefined) { + catEntry = accum[cat_id] = { count: 0 }; + } + catEntry.count += 1; + } + return accum; + }, + {} as Record + ); +} \ No newline at end of file diff --git a/public/ts/list_elem_detailed.ts b/public/ts/list_elem_detailed.ts index 4feaabf..e3014db 100644 --- a/public/ts/list_elem_detailed.ts +++ b/public/ts/list_elem_detailed.ts @@ -1,5 +1,5 @@ import { ListElementRenderer } from "./list_elem_renderer.js"; -import { fillAuthorDiv, getElementForCategory } from "./list_item_shared.js"; +import { fillAuthorDiv } from "./list_item_shared.js"; import { fabric_category_id } from "./initCategoriesSidebar.js"; import { Mod } from "./mod_types.js"; import { formatNumberCompact } from "./number_formatter.js"; @@ -136,7 +136,7 @@ class DetailedListElementRendererImpl extends ListElementRenderer< // if not "Fabric" if (category !== fabric_category_id) { const catElem = document.createElement("li"); - catElem.textContent = CATEGORIES.get()[category].name; + catElem.textContent = CATEGORIES.BY_ID[category].name; categories.appendChild(catElem); } } diff --git a/public/ts/list_item_shared.ts b/public/ts/list_item_shared.ts index 2746978..ebb0b7e 100644 --- a/public/ts/list_item_shared.ts +++ b/public/ts/list_item_shared.ts @@ -1,17 +1,25 @@ +import { Signal } from "signal-polyfill"; import { Author, Mod } from "./mod_types.js"; import { createCurseAuthorIcon, createModrinthAuthorIcon, } from "./platform_links.js"; +import { CATEGORIES, CategoryEl } from "./initCategoriesSidebar.js"; -var modCategoryElements: (() => Node)[]; +type ModCategoryElementFn = (category: CategoryEl) => (() => Node); +let modCategoryElementFn: ModCategoryElementFn; -export function setModCategoryElements(renderers: (() => Node)[]) { - modCategoryElements = renderers; +var modCategoryElements = new Signal.Computed<(() => Node)[]>(() => { + const categories = CATEGORIES.BY_ID; + return categories.map(modCategoryElementFn); +}); + +export function setModCategoryElementFn(fn: ModCategoryElementFn) { + modCategoryElementFn = fn; } export function getElementForCategory(categoryId: number): () => Node { - return modCategoryElements[categoryId]; + return modCategoryElements.get()[categoryId]; } // LIST ITEM AUTHORS LIST diff --git a/public/ts/list_search.civet b/public/ts/list_search.civet index 911b6eb..1c1f504 100644 --- a/public/ts/list_search.civet +++ b/public/ts/list_search.civet @@ -1,6 +1,6 @@ import { DefaultListElementRenderer } from "./list_elem_default.js"; import { DetailedListElementRenderer } from "./list_elem_detailed.js"; -import { setModCategoryElements } from "./list_item_shared.js"; +import { setModCategoryElementFn } from "./list_item_shared.js"; import { batch_containers, init, @@ -12,7 +12,7 @@ import { } from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; -import { CATEGORIES } from "./initCategoriesSidebar" +import { CATEGORIES, CategoryEl } from "./initCategoriesSidebar" import { effect } from "./effect" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -69,18 +69,12 @@ const preInitializationCallbacks: (() => void)[] = []; preInitializationCallbacks.push( () => setResultsListElement(getElementById("search_results_list"))) -var modCategoryElements: (() => Node)[]; -preInitializationCallbacks.push(() => { - console.log("CREATE CATEGORY ELEMS", CATEGORIES.get()) - effect(() => { - modCategoryElements = CATEGORIES.get() - .map((c) => document.createElement("li") ||> .textContent = c.name) - .map((c) => () => c.cloneNode(true)); - - setModCategoryElements(modCategoryElements); - - }) -}); +setModCategoryElementFn( + (category: CategoryEl) => { + const templateEl = document.createElement("li") ||> .textContent = category.name; + return () => templateEl.cloneNode(true) + } +) function getLiHeight() { const fmt = (val: string) => val.slice(0, val.length - 2) diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index c0b9347..d033a10 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -1,6 +1,6 @@ import { DefaultListElementRenderer } from "./list_elem_default.js"; import { DetailedListElementRenderer } from "./list_elem_detailed.js"; -import { setModCategoryElements } from "./list_item_shared.js"; +import { setModCategoryElementFn } from "./list_item_shared.js"; import { batch_containers, init, @@ -12,8 +12,7 @@ import { } from "./mod_search_logic.js"; import { Mod } from "./mod_types.js"; import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; -import { IdempotentComponent } from "./idempotent_component" -import { CATEGORIES } from "./initCategoriesSidebar" +import { CATEGORIES, CategoryEl } from "./initCategoriesSidebar" import { effect } from "./effect" type ListElementRenderFn = (modData: Mod) => HTMLLIElement; @@ -70,18 +69,12 @@ const preInitializationCallbacks: (() => void)[] = []; preInitializationCallbacks.push( () => setResultsListElement(getElementById("search_results_list"))) -var modCategoryElements: (() => Node)[]; -preInitializationCallbacks.push(() =>( { - log: console.log("CREATE CATEGORY ELEMS", CATEGORIES.get()), - effect: effect(() => { - modCategoryElements = CATEGORIES.get() - .map((c) => { let ref;(ref = document.createElement("li")).textContent = c.name;return ref }) - .map((c) => () => c.cloneNode(true)); - - setModCategoryElements(modCategoryElements); - - }) -})); +setModCategoryElementFn( + (category: CategoryEl) => { + let ref;const templateEl =( (ref = document.createElement("li")).textContent = category.name,ref); + return () => templateEl.cloneNode(true) + } +) function getLiHeight() { const fmt = (val: string) => val.slice(0, val.length - 2) diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 2f2b256..ac31c2a 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -8,10 +8,10 @@ import { } from "./table_sort.js"; import { BaseMod, Mod, baseModToMod, versionOrd } from "./mod_types.js"; import { initMultiselectElement } from "./multiselect.js"; -import { CATEGORY_NAMES, MOD_DATA, TOTAL_MOD_COUNT } from "./search_page_state" +import { MOD_DATA } from "./search_page_state" import { PGlite } from "@electric-sql/pglite"; import { effect } from "./effect.js"; -import { CATEGORIES, initCategoriesSidebar, BoolMode, applyCategorySelections, getSelectedCategoryIds, updateCategoryModCounts } from "./initCategoriesSidebar.js"; +import { CATEGORIES, initCategoriesSidebar, BoolMode, getSelectedCategoryIds, updateFilteredCategoryModCounts } from "./initCategoriesSidebar.js"; let prevTime = performance.now(); function logtime(message: string) { @@ -110,7 +110,7 @@ var localLoader = new AsyncDataResourceLoader({ ]) .addResource(`${apiUrl}/Categories`, [ (jsonData) => { - CATEGORY_NAMES.set(jsonData); + CATEGORIES.NAMES.set(jsonData); console.log("categoryNames", jsonData); }, ]) @@ -243,7 +243,7 @@ var loader = new AsyncDataResourceLoader({ ]) .addResource(`${apiUrl}/Categories`, [ (jsonData) => { - CATEGORY_NAMES.set(jsonData); + CATEGORIES.NAMES.set(jsonData); console.log("categoryNames", jsonData); }, ]) @@ -463,9 +463,9 @@ export type SearchOptions = Readonly<{ // var searchOptions: SearchOptions; export function getSearchOptionsFromState(): SearchOptions { - const categories = CATEGORIES.get().map((cat) => ({ + const categories = CATEGORIES.BY_ID.map((cat) => ({ name: cat.name, - bool_mode: cat.htmlElement.bool_mode, + bool_mode: cat.boolMode, })); const categoryIncludes = categories .filter((cat) => cat.bool_mode === BoolMode.And) @@ -580,22 +580,6 @@ export function getSearchOptionsFromUrl(): SearchOptions { }; } -export function selectCategories({ - categoryIncludes, - categoryExcludes, -}: SearchOptions): void { - const categories = CATEGORIES.get(); - categoryIncludes?.forEach((element) => { - categories.find((cat) => cat.name === element)!.htmlElement.bool_mode = - BoolMode.And; - }); - categoryExcludes?.forEach((element) => { - categories.find((cat) => cat.name === element)!.htmlElement.bool_mode = - BoolMode.Not; - }); - applyCategorySelections(); -} - function search( queryText: string, search_objects: Mod[], @@ -715,7 +699,7 @@ export function searchTextChanged(value?: string, resultsPersist?: boolean) { function updateSearchResults(results: Mod[]) { updateSearchResultsListElement(results); - updateCategoryModCounts(results); + updateFilteredCategoryModCounts(results); } //======= diff --git a/public/ts/search_page_state.ts b/public/ts/search_page_state.ts index 1f80717..8e363c3 100644 --- a/public/ts/search_page_state.ts +++ b/public/ts/search_page_state.ts @@ -3,5 +3,5 @@ import { Mod } from "./mod_types"; export const MOD_DATA = new Signal.State([]); export const TOTAL_MOD_COUNT = new Signal.State(null); -export const CATEGORY_NAMES = new Signal.State([]); +// export const CATEGORY_NAMES = ; From 9958cbed1ee6dcc3efbcd0407c5d4cf9218f75bc Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 17:08:51 -0500 Subject: [PATCH 08/11] simplify some initialization --- public/ts/initCategoriesSidebar.ts | 1 - public/ts/mod_search_logic.ts | 202 ++++++----------------------- public/ts/sql_loader.ts | 193 +++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 161 deletions(-) create mode 100644 public/ts/sql_loader.ts diff --git a/public/ts/initCategoriesSidebar.ts b/public/ts/initCategoriesSidebar.ts index 7748e64..da5b651 100644 --- a/public/ts/initCategoriesSidebar.ts +++ b/public/ts/initCategoriesSidebar.ts @@ -249,7 +249,6 @@ export class CategoryEl { totalModCount: number, filteredModCount: number | null ) { - console.log("renderModCounts", totalModCount, filteredModCount) this._countElement.textContent = buildCategoryCountStr( totalModCount, filteredModCount diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index ac31c2a..750d8f9 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -12,6 +12,7 @@ import { MOD_DATA } from "./search_page_state" import { PGlite } from "@electric-sql/pglite"; import { effect } from "./effect.js"; import { CATEGORIES, initCategoriesSidebar, BoolMode, getSelectedCategoryIds, updateFilteredCategoryModCounts } from "./initCategoriesSidebar.js"; +import { getDbModData, updateDatabase } from "./sql_loader.js"; let prevTime = performance.now(); function logtime(message: string) { @@ -68,35 +69,11 @@ var localLoader = new AsyncDataResourceLoader({ return undefined; } console.log("HAVE DB"); - // const res = await db.query("SELECT COUNT(*) FROM mod_data;"); - // // @ts-ignore - // console.log("CHECK DB RES", res, res.rows[0].count); - - // @ts-ignore - // if (res.rows[0].count) { - console.log("HAVE ROWS"); - - const temp_mod_data = await db.query( - `SELECT - id, - name, - mr_slug, - cf_slug, - summary, - categories, - authors, - date_released as "dateReleased", - date_modified as "dateModified", - download_count as "downloadCount", - mc_versions, - s_name, - s_latest_mc_version as "s_latestMCVersion" - FROM mod_data ORDER BY download_count DESC LIMIT 100; - ` - ); + + const temp_mod_data = await getDbModData(db, 100); logtime("Start local db query...done!"); - return temp_mod_data.rows as Mod[]; - // } + + return temp_mod_data; }, [ async (temp_mod_data) => { if (!temp_mod_data) { @@ -107,7 +84,13 @@ var localLoader = new AsyncDataResourceLoader({ setModData(temp_mod_data); mod_data.sort((a, b) => b.downloadCount - a.downloadCount); }, - ]) + ]); + +configureModsLoader(localLoader); + +var categoriesLoader = new AsyncDataResourceLoader({ + completionWaitForDCL: true, +}) .addResource(`${apiUrl}/Categories`, [ (jsonData) => { CATEGORIES.NAMES.set(jsonData); @@ -117,11 +100,9 @@ var localLoader = new AsyncDataResourceLoader({ .addCompletionFunc(() => { effect(() => { initCategoriesSidebar(); - }) + }) }); -configureModsLoader(localLoader); - // Load mod data from external file var loader = new AsyncDataResourceLoader({ completionWaitForDCL: true, @@ -135,123 +116,13 @@ var loader = new AsyncDataResourceLoader({ console.log("mod_data", mod_data); console.log("TABLE CREATED IF NEEDED"); - //-- DROP TABLE IF EXISTS mod_data; - // - // First create the table if it doesn't exist - await db.exec(` - CREATE TABLE IF NOT EXISTS mod_data ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - mr_slug TEXT, - cf_slug TEXT, - summary TEXT, - categories INT4[], -- Text array for categories - authors JSONB, -- JSONB for authors - date_released TIMESTAMP, - date_modified TIMESTAMP, - download_count BIGINT, - mc_versions TEXT[], -- Text array for versions - s_name TEXT, - s_latest_mc_version NUMERIC, - s_date_modified BIGINT, - latest_mc_version TEXT, - s_author TEXT - ); - `); - - // Batch processing using PGlite's transaction feature - await db.transaction(async (tx) => { - // Process in batches to avoid memory issues - const BATCH_SIZE = 500; - - for (let i = 0; i < mod_data.length; i += BATCH_SIZE) { - const batch = mod_data.slice(i, i + BATCH_SIZE); - console.log( - `Processing batch ${ - Math.floor(i / BATCH_SIZE) + 1 - } of ${Math.ceil(mod_data.length / BATCH_SIZE)}` - ); - - // Create a large multi-value INSERT statement - let valuesSql = []; - let params = []; - let paramIndex = 1; - - for (const mod of batch) { - // Add placeholders for this row - valuesSql.push(`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, - $${paramIndex++}::INT4[], $${paramIndex++}::jsonb, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, - $${paramIndex++}::text[], $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, - $${paramIndex++})`); - - // Add values to params array - params.push( - mod.id, - mod.name, - mod.mr_slug, - mod.cf_slug, - mod.summary, - mod.categories, - JSON.stringify(mod.authors), - mod.dateReleased, - mod.dateModified, - mod.downloadCount, - mod.mc_versions, - mod.s_name, - mod.s_latestMCVersion, - mod.s_dateModified, - mod.latestMCVersion, - mod.s_author - ); - } - - // Build the complete query with all batch values - const query = ` - INSERT INTO mod_data ( - id, name, mr_slug, cf_slug, summary, categories, authors, - date_released, date_modified, download_count, mc_versions, - s_name, s_latest_mc_version, s_date_modified, latest_mc_version, s_author - ) - VALUES ${valuesSql.join(",")} - ON CONFLICT (id) - DO UPDATE SET - name = EXCLUDED.name, - mr_slug = EXCLUDED.mr_slug, - cf_slug = EXCLUDED.cf_slug, - summary = EXCLUDED.summary, - categories = EXCLUDED.categories, - authors = EXCLUDED.authors, - date_released = EXCLUDED.date_released, - date_modified = EXCLUDED.date_modified, - download_count = EXCLUDED.download_count, - mc_versions = EXCLUDED.mc_versions, - s_name = EXCLUDED.s_name, - s_latest_mc_version = EXCLUDED.s_latest_mc_version, - s_date_modified = EXCLUDED.s_date_modified, - latest_mc_version = EXCLUDED.latest_mc_version, - s_author = EXCLUDED.s_author - `; - - await tx.query(query, params); - } - }); + await updateDatabase(db, mod_data) console.log( "DONE SETUP", await db.query("SELECT * FROM mod_data;") ); }, - ]) - .addResource(`${apiUrl}/Categories`, [ - (jsonData) => { - CATEGORIES.NAMES.set(jsonData); - console.log("categoryNames", jsonData); - }, - ]) - .addCompletionFunc(() => { - effect(() => { - initCategoriesSidebar(); - }) - }); + ]); configureModsLoader(loader); var timestamp: string; @@ -262,6 +133,13 @@ function registerOnLoad(fn: () => void): void { loader.addCompletionFunc(fn); } +const hasRunKeys = new Set(); +function runOnce(key: string, fn: (() => void) | (() => Promise)) { + if (!hasRunKeys.has(key)) { + hasRunKeys.add(key); + fn(); + } +} function configureModsLoader(loader: AsyncDataResourceLoader): void { loader .addCompletionFunc(() => { @@ -287,22 +165,25 @@ function configureModsLoader(loader: AsyncDataResourceLoader): void { ) ) .addCompletionFunc(() => { - const searchOptions = getSearchOptionsFromUrl(); - setSortMode({ - sortField: searchOptions.sortField, - sortDirection: searchOptions.sortDirection, - }); - currentSelectedVersions = - searchOptions.versions?.map( - (str) => [str, versionOrd(str)] as [string, number] - ) ?? []; - console.log(searchOptions, currentSelectedVersions); - searchTextChanged(undefined); - registerSortListener(({ sortMode: sortField, sortDirection }) => { - updateUrlFromSearchOptions({ - ...getSearchOptionsFromState(), - sortField, - sortDirection, + runOnce("URL State", () => { + // sync state to/from URL + const searchOptions = getSearchOptionsFromUrl(); + setSortMode({ + sortField: searchOptions.sortField, + sortDirection: searchOptions.sortDirection, + }); + currentSelectedVersions = + searchOptions.versions?.map( + (str) => [str, versionOrd(str)] as [string, number] + ) ?? []; + console.log(searchOptions, currentSelectedVersions); + searchTextChanged(undefined); + registerSortListener(({ sortMode: sortField, sortDirection }) => { + updateUrlFromSearchOptions({ + ...getSearchOptionsFromState(), + sortField, + sortDirection, + }); }); }); }) @@ -393,6 +274,7 @@ function configureModsLoader(loader: AsyncDataResourceLoader): void { } function init() { + categoriesLoader.fetchResources(); localLoader.fetchResources(); loader.fetchResources(); } diff --git a/public/ts/sql_loader.ts b/public/ts/sql_loader.ts new file mode 100644 index 0000000..502e626 --- /dev/null +++ b/public/ts/sql_loader.ts @@ -0,0 +1,193 @@ +import { PGlite, Transaction } from "@electric-sql/pglite"; +import { Mod } from "./mod_types"; + +// Process large data sets without freezing the UI +async function processBatchesNonBlocking(db: PGlite, mod_data: Mod[]) { + const BATCH_SIZE = 500; + const totalBatches = Math.ceil(mod_data.length / BATCH_SIZE); + + // Create a promise that will resolve when all processing is complete + return new Promise(async (resolve, reject) => { + try { + // Use transaction for the entire operation + await db.transaction(async (tx) => { + let batchIndex = 0; + + // Process one batch at a time with UI updates in between + async function processNextBatch() { + if (batchIndex >= mod_data.length) { + // All batches processed + resolve(); + return; + } + + // Schedule UI update before processing + await new Promise((requestAnimationFrameResolve) => { + requestAnimationFrame(() => { + requestAnimationFrameResolve(); + }); + }); + + // Get the current batch + const batch = mod_data.slice( + batchIndex, + batchIndex + BATCH_SIZE + ); + + await new Promise((microtaskResolve) => { + queueMicrotask(async () => { + try { + console.log( + `Processing batch ${ + Math.floor(batchIndex / BATCH_SIZE) + 1 + } of ${totalBatches}` + ); + + await processSingleBatch(tx, batch); + batchIndex += BATCH_SIZE; + microtaskResolve(); + } catch (error) { + reject(error); + } + }); + }); + + // Schedule the next batch processing + await processNextBatch(); + } + + // Start processing batches + await processNextBatch(); + }); + } catch (error) { + reject(error); + } + }); +} + +// Helper function to process a single batch of data +async function processSingleBatch(tx: Transaction, batch: Mod[]) { + // Create a large multi-value INSERT statement + let valuesSql = []; + let params = []; + let paramIndex = 1; + + for (const mod of batch) { + // Add placeholders for this row + valuesSql.push(`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++}::INT4[], $${paramIndex++}::jsonb, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++}::text[], $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, + $${paramIndex++})`); + + // Add values to params array + params.push( + mod.id, + mod.name, + mod.mr_slug, + mod.cf_slug, + mod.summary, + mod.categories, + JSON.stringify(mod.authors), + mod.dateReleased, + mod.dateModified, + mod.downloadCount, + mod.mc_versions, + mod.s_name, + mod.s_latestMCVersion, + mod.s_dateModified, + mod.latestMCVersion, + mod.s_author + ); + } + + // Build the complete query with all batch values + const query = ` + INSERT INTO mod_data ( + id, name, mr_slug, cf_slug, summary, categories, authors, + date_released, date_modified, download_count, mc_versions, + s_name, s_latest_mc_version, s_date_modified, latest_mc_version, s_author + ) + VALUES ${valuesSql.join(",")} + ON CONFLICT (id) + DO UPDATE SET + name = EXCLUDED.name, + mr_slug = EXCLUDED.mr_slug, + cf_slug = EXCLUDED.cf_slug, + summary = EXCLUDED.summary, + categories = EXCLUDED.categories, + authors = EXCLUDED.authors, + date_released = EXCLUDED.date_released, + date_modified = EXCLUDED.date_modified, + download_count = EXCLUDED.download_count, + mc_versions = EXCLUDED.mc_versions, + s_name = EXCLUDED.s_name, + s_latest_mc_version = EXCLUDED.s_latest_mc_version, + s_date_modified = EXCLUDED.s_date_modified, + latest_mc_version = EXCLUDED.latest_mc_version, + s_author = EXCLUDED.s_author + `; + + // Execute the query within the transaction + await tx.query(query, params); +} + +export async function updateDatabase(db: PGlite, mod_data: Mod[]) { + try { + setLoadingState(true); + await db.exec(` + CREATE TABLE IF NOT EXISTS mod_data ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + mr_slug TEXT, + cf_slug TEXT, + summary TEXT, + categories INT4[], -- Text array for categories + authors JSONB, -- JSONB for authors + date_released TIMESTAMP, + date_modified TIMESTAMP, + download_count BIGINT, + mc_versions TEXT[], -- Text array for versions + s_name TEXT, + s_latest_mc_version NUMERIC, + s_date_modified BIGINT, + latest_mc_version TEXT, + s_author TEXT + ); + `); + + await processBatchesNonBlocking(db, mod_data); + } catch (error) { + console.error("Error updating database:", error); + } finally { + setLoadingState(false); + } +} + +function setLoadingState(isLoading: boolean) { + const loadingElement = document.getElementById("loading-indicator"); + if (loadingElement) { + loadingElement.style.display = isLoading ? "block" : "none"; + } +} + +export async function getDbModData(db: PGlite, count: number): Promise { + const temp_mod_data = await db.query( + `SELECT + id, + name, + mr_slug, + cf_slug, + summary, + categories, + authors, + date_released as "dateReleased", + date_modified as "dateModified", + download_count as "downloadCount", + mc_versions, + s_name, + s_latest_mc_version as "s_latestMCVersion" + FROM mod_data ORDER BY download_count DESC LIMIT ${count}; + ` + ); + return temp_mod_data.rows as Mod[]; +} From 5b1ff416110eb58bdc8e00dd6f734d971ebbbc7b Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 17:37:12 -0500 Subject: [PATCH 09/11] versions filtering to own file, and fix some logic around snapshots --- public/ts/mod_search_logic.ts | 244 ++++++++---------------------- public/ts/util.ts | 16 ++ public/ts/versions_multiselect.ts | 124 +++++++++++++++ 3 files changed, 199 insertions(+), 185 deletions(-) create mode 100644 public/ts/versions_multiselect.ts diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 750d8f9..567e106 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -13,6 +13,7 @@ import { PGlite } from "@electric-sql/pglite"; import { effect } from "./effect.js"; import { CATEGORIES, initCategoriesSidebar, BoolMode, getSelectedCategoryIds, updateFilteredCategoryModCounts } from "./initCategoriesSidebar.js"; import { getDbModData, updateDatabase } from "./sql_loader.js"; +import { filterByVersion, initVersionsMultiSelect, SELECTED_VERSIONS } from "./versions_multiselect.js"; let prevTime = performance.now(); function logtime(message: string) { @@ -126,7 +127,6 @@ var loader = new AsyncDataResourceLoader({ configureModsLoader(loader); var timestamp: string; -var currentSelectedVersions: [string, number][] = []; function registerOnLoad(fn: () => void): void { localLoader.addCompletionFunc(fn); @@ -172,11 +172,12 @@ function configureModsLoader(loader: AsyncDataResourceLoader): void { sortField: searchOptions.sortField, sortDirection: searchOptions.sortDirection, }); - currentSelectedVersions = + SELECTED_VERSIONS.set( searchOptions.versions?.map( (str) => [str, versionOrd(str)] as [string, number] - ) ?? []; - console.log(searchOptions, currentSelectedVersions); + ) ?? [] + ); + console.log(searchOptions, SELECTED_VERSIONS.get()); searchTextChanged(undefined); registerSortListener(({ sortMode: sortField, sortDirection }) => { updateUrlFromSearchOptions({ @@ -187,90 +188,7 @@ function configureModsLoader(loader: AsyncDataResourceLoader): void { }); }); }) - .addCompletionFunc(() => { - const versionNums = new Set(); - const versions: [string, number][] = []; - for (let i = 0; i < mod_data.length; i++) { - const m = mod_data[i]; - if (!versionNums.has(m.s_latestMCVersion)) { - versions.push([m.latestMCVersion, m.s_latestMCVersion]); - } - versionNums.add(m.s_latestMCVersion); - } - - versions.sort((a, b) => b[1] - a[1]); // descending - // var options = ["option a", "option b", "option c"]; - getSearchOptionsFromUrl().versions; - - const showSnapshotsLabel = document.createElement("label"); - showSnapshotsLabel.textContent = "show snapshots"; - showSnapshotsLabel.classList.add("button"); - const showSnapshotsCheckbox = document.createElement("input"); - showSnapshotsCheckbox.type = "checkbox"; - showSnapshotsCheckbox.id = "snapshot_toggle"; - const getSnapshotsLabel = () => { - showSnapshotsLabel.textContent = "show snapshots"; - showSnapshotsLabel.appendChild(showSnapshotsCheckbox); - return showSnapshotsLabel; - }; - - const setSelectedVersions = (newVersions: [string, number][]) => { - currentSelectedVersions = newVersions; - searchTextChanged(undefined, true); - updateUrlFromSearchOptions({ - ...getSearchOptionsFromState(), - }); - - console.log(currentSelectedVersions); - }; - const initVersionsMultiselect = ( - versionsForMultiselect: [string, number][] - ) => { - initMultiselectElement({ - rootElement: getElementById("version_multiselect"), - options: versionsForMultiselect, - setSelectedValues: (setter) => { - setSelectedVersions(setter(currentSelectedVersions)); - }, - currentValues: currentSelectedVersions, - renderValue: (val) => val[0], - key: (val) => val[1], // gets the version in num form, - leadingChildren: [getSnapshotsLabel()], - }); - }; - const snapshotRegex = /[a-z]/i; - const anyAreSnapshots = (versionsToTest: [string, number][]) => - versionsToTest.some((v) => !snapshotRegex.test(v[0])); - initVersionsMultiselect( - anyAreSnapshots(currentSelectedVersions) - ? versions - : versions.filter((v) => !snapshotRegex.test(v[0])) - ); - - showSnapshotsCheckbox.addEventListener("change", (e) => { - const shouldShowSnapshots = (e.target as HTMLInputElement) - .checked; - clearInner(getElementById("version_multiselect")); - - const versionsForMultiselect = shouldShowSnapshots - ? versions - : versions.filter((v) => !snapshotRegex.test(v[0])); - - setSelectedVersions( - shouldShowSnapshots - ? currentSelectedVersions - : currentSelectedVersions.filter( - (v) => !snapshotRegex.test(v[0]) - ) - ); - initVersionsMultiselect(versionsForMultiselect); - console.log( - shouldShowSnapshots, - versionsForMultiselect, - versions - ); - }); - }); + .addCompletionFunc(() => initVersionsMultiSelect(mod_data)); } function init() { @@ -364,7 +282,7 @@ export function getSearchOptionsFromState(): SearchOptions { categoryExcludes, sortField: sortState.sortMode, sortDirection: sortState.sortDirection, - versions: currentSelectedVersions.map(([str, num]) => str), + versions: SELECTED_VERSIONS.get().map(([str, num]) => str), }; } @@ -375,67 +293,68 @@ const urlDecodeCategories = (urlEncString: string | undefined | null) => urlEncString ? urlEncString.split(encodeURIComponent(",")) : undefined; export function updateUrlFromSearchOptions(options: SearchOptions) { - if ("URLSearchParams" in window) { - var searchParams = new URLSearchParams(window.location.search); + if (!("URLSearchParams" in window)) { + return; + } + var searchParams = new URLSearchParams(window.location.search); - { - if (options.search) { - searchParams.set("search", options.search); - } else { - searchParams.delete("search"); - } + { // SEARCH + if (options.search) { + searchParams.set("search", options.search); + } else { + searchParams.delete("search"); } + } - { - if ( - options.categoryIncludes && - options.categoryIncludes.length > 0 - ) { - searchParams.set( - "categoryIncludes", - urlFormatCategories(options.categoryIncludes) - ); - } else { - searchParams.delete("categoryIncludes"); - } - - if ( - options.categoryExcludes && - options.categoryExcludes.length > 0 - ) { - searchParams.set( - "categoryExcludes", - urlFormatCategories(options.categoryExcludes) - ); - } else { - searchParams.delete("categoryExcludes"); - } + { // CATEGORIES + if ( + options.categoryIncludes && + options.categoryIncludes.length > 0 + ) { + searchParams.set( + "categoryIncludes", + urlFormatCategories(options.categoryIncludes) + ); + } else { + searchParams.delete("categoryIncludes"); } - { - if (options.sortField && options.sortDirection) { - searchParams.set("sortField", options.sortField); - searchParams.set("sortDirection", options.sortDirection); - } else { - searchParams.delete("sortField"); - searchParams.delete("sortDirection"); - } + if ( + options.categoryExcludes && + options.categoryExcludes.length > 0 + ) { + searchParams.set( + "categoryExcludes", + urlFormatCategories(options.categoryExcludes) + ); + } else { + searchParams.delete("categoryExcludes"); } + } - { - if (options.versions && options.versions.length > 0) { - searchParams.set("versions", options.versions.join("|")); - } else { - searchParams.delete("versions"); - } + { // SORTING + if (options.sortField && options.sortDirection) { + searchParams.set("sortField", options.sortField); + searchParams.set("sortDirection", options.sortDirection); + } else { + searchParams.delete("sortField"); + searchParams.delete("sortDirection"); } + } - const queryAsText = searchParams.toString(); - const newRelativePathQuery = - window.location.pathname + - (queryAsText.length > 0 ? "?" + queryAsText : ""); - history.replaceState(null, "", newRelativePathQuery); + { // VERSIONS FILTER + if (options.versions && options.versions.length > 0) { + searchParams.set("versions", options.versions.join("|")); + } else { + searchParams.delete("versions"); + } } + + const queryAsText = searchParams.toString(); + const newRelativePathQuery = + window.location.pathname + + (queryAsText.length > 0 ? "?" + queryAsText : ""); + history.replaceState(null, "", newRelativePathQuery); } function getUrlSearchValue(): string | undefined { @@ -511,36 +430,6 @@ function search( registerSortListener(() => searchTextChanged(undefined, true)); -const filterByVersion = (results: Mod[]) => { - if (currentSelectedVersions && currentSelectedVersions.length > 0) { - const selectedVersionStrings = currentSelectedVersions.map( - ([str, num]) => str - ); - - switch ((window as any).fiberVersionFilterMode) { - case "allMatch": - return results.filter((mod) => - mod.mc_versions.every((val) => - selectedVersionStrings.includes(val) - ) - ); - case "noneMatch": - return results.filter((mod) => - mod.mc_versions.every( - (val) => !selectedVersionStrings.includes(val) - ) - ); - default: - return results.filter((mod) => - mod.mc_versions.some((val) => - selectedVersionStrings.includes(val) - ) - ); - } - } - return results; -}; - //================ // Input Handling //================ @@ -969,21 +858,6 @@ function createStyleSheet(id: string, media?: string) { } return el.sheet; } -/** - * - * @param {HTMLElement} node - */ -function clearInner(node: HTMLElement) { - while (node.hasChildNodes()) { - clear(node.firstChild!); - } -} -function clear(node: Node) { - while (node.hasChildNodes()) { - clear(node.firstChild!); - } - node.parentNode?.removeChild(node); -} function clearShallow(node: HTMLElement) { for (const c of node.childNodes) { diff --git a/public/ts/util.ts b/public/ts/util.ts index a83a237..f04ea91 100644 --- a/public/ts/util.ts +++ b/public/ts/util.ts @@ -147,3 +147,19 @@ export const getElementById = (id: string) => { } return elem; }; + +/** + * + * @param {HTMLElement} node + */ +export function clearInner(node: HTMLElement) { + while (node.hasChildNodes()) { + clear(node.firstChild!); + } +} +export function clear(node: Node) { + while (node.hasChildNodes()) { + clear(node.firstChild!); + } + node.parentNode?.removeChild(node); +} diff --git a/public/ts/versions_multiselect.ts b/public/ts/versions_multiselect.ts new file mode 100644 index 0000000..b955f5f --- /dev/null +++ b/public/ts/versions_multiselect.ts @@ -0,0 +1,124 @@ +import { Signal } from "signal-polyfill"; +import { + getSearchOptionsFromState, + getSearchOptionsFromUrl, + searchTextChanged, + updateUrlFromSearchOptions, +} from "./mod_search_logic"; +import { Mod } from "./mod_types"; +import { initMultiselectElement } from "./multiselect"; +import { clearInner, getElementById } from "./util"; + +export var SELECTED_VERSIONS = new Signal.State<[string, number][]>([]); + +export function initVersionsMultiSelect(mod_data: Mod[]): void { + const versionNums = new Set(); + const versions: [string, number][] = []; + for (let i = 0; i < mod_data.length; i++) { + const m = mod_data[i]; + if (!versionNums.has(m.s_latestMCVersion)) { + versions.push([m.latestMCVersion, m.s_latestMCVersion]); + } + versionNums.add(m.s_latestMCVersion); + } + + versions.sort((a, b) => b[1] - a[1]); // descending + // var options = ["option a", "option b", "option c"]; + getSearchOptionsFromUrl().versions; + + const showSnapshotsLabel = document.createElement("label"); + showSnapshotsLabel.textContent = "show snapshots"; + showSnapshotsLabel.classList.add("button"); + const showSnapshotsCheckbox = document.createElement("input"); + showSnapshotsCheckbox.type = "checkbox"; + showSnapshotsCheckbox.id = "snapshot_toggle"; + const getSnapshotsLabel = () => { + showSnapshotsLabel.textContent = "show snapshots"; + showSnapshotsLabel.appendChild(showSnapshotsCheckbox); + return showSnapshotsLabel; + }; + + const setSelectedVersions = (newVersions: [string, number][]) => { + SELECTED_VERSIONS.set(newVersions); + searchTextChanged(undefined, true); + updateUrlFromSearchOptions({ + ...getSearchOptionsFromState(), + }); + + console.log(SELECTED_VERSIONS.get()); + }; + const initVersionsMultiselect = ( + versionsForMultiselect: [string, number][] + ) => { + clearInner(getElementById("version_multiselect")) + initMultiselectElement({ + rootElement: getElementById("version_multiselect"), + options: versionsForMultiselect, + setSelectedValues: (setter) => { + setSelectedVersions(setter(SELECTED_VERSIONS.get())); + }, + currentValues: SELECTED_VERSIONS.get(), + renderValue: (val) => val[0], + key: (val) => val[1], // gets the version in num form, + leadingChildren: [getSnapshotsLabel()], + }); + }; + const snapshotRegex = /[a-z]/i; + const anyAreSnapshots = (versionsToTest: [string, number][]) => + versionsToTest.some((v) => snapshotRegex.test(v[0])); + const initialAreAnySnapshots = anyAreSnapshots(SELECTED_VERSIONS.get()); + initVersionsMultiselect( + initialAreAnySnapshots + ? versions + : versions.filter((v) => !snapshotRegex.test(v[0])) + ); + + showSnapshotsCheckbox.checked = initialAreAnySnapshots; + showSnapshotsCheckbox.addEventListener("change", (e) => { + const shouldShowSnapshots = (e.target as HTMLInputElement).checked; + clearInner(getElementById("version_multiselect")); + + const versionsForMultiselect = shouldShowSnapshots + ? versions + : versions.filter((v) => !snapshotRegex.test(v[0])); + + setSelectedVersions( + shouldShowSnapshots + ? SELECTED_VERSIONS.get() + : SELECTED_VERSIONS.get().filter( + (v) => !snapshotRegex.test(v[0]) + ) + ); + initVersionsMultiselect(versionsForMultiselect); + console.log(shouldShowSnapshots, versionsForMultiselect, versions); + }); +} +export const filterByVersion = (results: Mod[]) => { + if (SELECTED_VERSIONS.get() && SELECTED_VERSIONS.get().length > 0) { + const selectedVersionStrings = SELECTED_VERSIONS.get().map( + ([str, num]) => str + ); + + switch ((window as any).fiberVersionFilterMode) { + case "allMatch": + return results.filter((mod) => + mod.mc_versions.every((val) => + selectedVersionStrings.includes(val) + ) + ); + case "noneMatch": + return results.filter((mod) => + mod.mc_versions.every( + (val) => !selectedVersionStrings.includes(val) + ) + ); + default: + return results.filter((mod) => + mod.mc_versions.some((val) => + selectedVersionStrings.includes(val) + ) + ); + } + } + return results; +}; From 7ad2b952d734c0108a7472f6e36d259c12b1ceba Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 18:15:00 -0500 Subject: [PATCH 10/11] remove `civet`. It's really, really cool, but I just spend 30m on a line-not-executed bug because of a `;` --- package.json | 3 - pnpm-lock.yaml | 101 ------------------------- public/ts/list_search.civet | 137 ---------------------------------- public/ts/list_search.ts | 10 ++- public/ts/mod_search_logic.ts | 3 +- public/ts/multiselect.civet | 118 ----------------------------- 6 files changed, 8 insertions(+), 364 deletions(-) delete mode 100644 public/ts/list_search.civet delete mode 100644 public/ts/multiselect.civet diff --git a/package.json b/package.json index 234c3fa..30e6357 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,13 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "civet": "civet -c public/ts/*.civet -o .ts", "build": "tsc", - "__build:civet": "tsc && civet --js -c public/ts/*.civet -o ./public/ts-compiled/.js", "dev": "rsbuild dev" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@danielx/civet": "^0.9.7", "@rsbuild/core": "^1.2.14", "typescript": "^5.8.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 860ff02..303821d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,9 +15,6 @@ importers: specifier: ^0.2.2 version: 0.2.2 devDependencies: - '@danielx/civet': - specifier: ^0.9.7 - version: 0.9.7(typescript@5.8.2) '@rsbuild/core': specifier: ^1.2.14 version: 1.2.14 @@ -27,34 +24,9 @@ importers: packages: - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@danielx/civet@0.9.7': - resolution: {integrity: sha512-T3rCaTMMWnQsII9MYL8ZTfFAnf738gXihNcHZXb/zKRqcKUR28mfnjbsv8a/gC90Dr+cnlq8WrIu9BaDyfU0Bw==} - engines: {node: '>=19 || ^18.6.0 || ^16.17.0'} - hasBin: true - peerDependencies: - typescript: ^4.5 || ^5.0 - yaml: ^2.4.5 - peerDependenciesMeta: - yaml: - optional: true - '@electric-sql/pglite@0.2.17': resolution: {integrity: sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==} - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@module-federation/error-codes@0.8.4': resolution: {integrity: sha512-55LYmrDdKb4jt+qr8qE8U3al62ZANp3FhfVaNPOaAmdTh0jHdD8M3yf5HKFlr5xVkVO4eV/F/J2NCfpbh+pEXQ==} @@ -142,31 +114,12 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@typescript/vfs@1.6.1': - resolution: {integrity: sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==} - peerDependencies: - typescript: '*' - - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} - engines: {node: '>=0.4.0'} - hasBin: true - caniuse-lite@1.0.30001701: resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==} core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - isomorphic-rslog@0.0.6: resolution: {integrity: sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A==} engines: {node: '>=14.17.6'} @@ -175,9 +128,6 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - signal-polyfill@0.2.2: resolution: {integrity: sha512-p63Y4Er5/eMQ9RHg0M0Y64NlsQKpiu6MDdhBXpyywRuWiPywhJTpKJ1iB5K2hJEbFZ0BnDS7ZkJ+0AfTuL37Rg==} @@ -189,39 +139,10 @@ packages: engines: {node: '>=14.17'} hasBin: true - unplugin@2.2.0: - resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==} - engines: {node: '>=18.12.0'} - - webpack-virtual-modules@0.6.2: - resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - snapshots: - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@danielx/civet@0.9.7(typescript@5.8.2)': - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@typescript/vfs': 1.6.1(typescript@5.8.2) - typescript: 5.8.2 - unplugin: 2.2.0 - transitivePeerDependencies: - - supports-color - '@electric-sql/pglite@0.2.17': {} - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - '@module-federation/error-codes@0.8.4': {} '@module-federation/runtime-tools@0.8.4': @@ -307,38 +228,16 @@ snapshots: dependencies: tslib: 2.8.1 - '@typescript/vfs@1.6.1(typescript@5.8.2)': - dependencies: - debug: 4.4.0 - typescript: 5.8.2 - transitivePeerDependencies: - - supports-color - - acorn@8.14.0: {} - caniuse-lite@1.0.30001701: {} core-js@3.40.0: {} - debug@4.4.0: - dependencies: - ms: 2.1.3 - isomorphic-rslog@0.0.6: {} jiti@2.4.2: {} - ms@2.1.3: {} - signal-polyfill@0.2.2: {} tslib@2.8.1: {} typescript@5.8.2: {} - - unplugin@2.2.0: - dependencies: - acorn: 8.14.0 - webpack-virtual-modules: 0.6.2 - - webpack-virtual-modules@0.6.2: {} diff --git a/public/ts/list_search.civet b/public/ts/list_search.civet deleted file mode 100644 index 1c1f504..0000000 --- a/public/ts/list_search.civet +++ /dev/null @@ -1,137 +0,0 @@ -import { DefaultListElementRenderer } from "./list_elem_default.js"; -import { DetailedListElementRenderer } from "./list_elem_detailed.js"; -import { setModCategoryElementFn } from "./list_item_shared.js"; -import { - batch_containers, - init, - initSearch, - registerOnLoad, - resultsListElement, - setLiHeight, - setResultsListElement, -} from "./mod_search_logic.js"; -import { Mod } from "./mod_types.js"; -import { executeIfWhenDOMContentLoaded, getElementById } from "./util.js"; -import { CATEGORIES, CategoryEl } from "./initCategoriesSidebar" -import { effect } from "./effect" - -type ListElementRenderFn = (modData: Mod) => HTMLLIElement; - -const createListElement: ListElementRenderFn = DefaultListElementRenderer(); - -const createListElementDetailed: ListElementRenderFn = - DetailedListElementRenderer(); - -function createBatch(batchIdx: number, data_batches: Mod[][]): void { - for (const result_data of data_batches[batchIdx]) { - try { - batch_containers[batchIdx].appendChild( - currentListCreationFunc(result_data), - ); - } catch (err) { - batch_containers[batchIdx] - .appendChild(document.createElement("li")) - .setAttribute("class", "item"); - - console.warn(err); - console.warn(result_data); - } - }; -} - -// // Logic createListElement, true, LI_HEIGHT, BATCH_SIZE, createBatch -// type ModWithElem = Mod & { -// elem: HTMLElement; -// _elem: HTMLElement; -// _elemFn: () => HTMLElement; -// getElem: () => HTMLElement; -// }; -// -// loader.addCompletionFunc(() => { -// const MAX_FAILS = 100; -// let failCount = 0; -// for (const mod of mod_data as ModWithElem[]) { -// try { -// // mod.elem = createListElement(mod); -// // mod._elemFn = () => mod._elem = createListElement(mod); -// } catch (err) { -// console.warn(`Could not load elem for mod`); -// failCount++; -// if (failCount > MAX_FAILS) { -// break; -// } -// } -// } -// }); - -const preInitializationCallbacks: (() => void)[] = []; - -preInitializationCallbacks.push( - () => setResultsListElement(getElementById("search_results_list"))) - -setModCategoryElementFn( - (category: CategoryEl) => { - const templateEl = document.createElement("li") ||> .textContent = category.name; - return () => templateEl.cloneNode(true) - } -) - -function getLiHeight() { - const fmt = (val: string) => val.slice(0, val.length - 2) - const style = getComputedStyle(resultsListElement); - return parseInt(fmt(style.getPropertyValue("--item-height"))) + 2; -} - -function getLiHeightDetailed() { - const fmt = (val: string) => val.slice(0, val.length - 2) - const style = getComputedStyle(resultsListElement); - - return parseInt(fmt(style.getPropertyValue("--item-height"))) + 36 + 8 + 2; -} - -var viewModes = [createListElement, createListElementDetailed]; -var modeLiHeights = [getLiHeight, getLiHeightDetailed]; -const CURRENT_LIST_VIEW_IDX_STORAGE_KEY = "currentListViewIdx"; -var currentViewIdx = - Number(window.localStorage.getItem(CURRENT_LIST_VIEW_IDX_STORAGE_KEY)) ?? 0; -var currentListCreationFunc = viewModes[0]; -function updateViewModes() { - currentListCreationFunc = viewModes[currentViewIdx]; - setLiHeight(modeLiHeights[currentViewIdx]()); -} -function cycleListViewModes() { - currentViewIdx++; - if (currentViewIdx >= viewModes.length) { - currentViewIdx = 0; - } - window.localStorage.setItem( - CURRENT_LIST_VIEW_IDX_STORAGE_KEY, - currentViewIdx.toString(), - ); - updateViewModes(); -} - -window.matchMedia("only screen and (max-width: 1000px)").onchange = () => { - updateViewModes(); -}; - -executeIfWhenDOMContentLoaded(() => { - getElementById("list_view_cycle_button").addEventListener( - "click", - cycleListViewModes, - ); -}); - -initSearch({ - results_persist: true, - batchCreationFunc: createBatch, - lazyLoadBatches: true, - batch_size: 20, - li_height: modeLiHeights[currentViewIdx], - preInitializationCallbacks -}); -registerOnLoad(() => { - updateViewModes(); -}); - -init(); diff --git a/public/ts/list_search.ts b/public/ts/list_search.ts index d033a10..a253704 100644 --- a/public/ts/list_search.ts +++ b/public/ts/list_search.ts @@ -71,7 +71,8 @@ preInitializationCallbacks.push( setModCategoryElementFn( (category: CategoryEl) => { - let ref;const templateEl =( (ref = document.createElement("li")).textContent = category.name,ref); + const templateEl = document.createElement("li"); + templateEl.textContent = category.name; return () => templateEl.cloneNode(true) } ) @@ -124,14 +125,15 @@ executeIfWhenDOMContentLoaded(() => { initSearch({ results_persist: true, + listElemCreationFunc: createListElement, batchCreationFunc: createBatch, lazyLoadBatches: true, batch_size: 20, li_height: modeLiHeights[currentViewIdx], preInitializationCallbacks }); -registerOnLoad(() =>( { - updateViewModes() {; } -})); +registerOnLoad(() => { + updateViewModes() +}); init(); diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 567e106..17bac2c 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -193,7 +193,7 @@ function configureModsLoader(loader: AsyncDataResourceLoader): void { function init() { categoriesLoader.fetchResources(); - localLoader.fetchResources(); + // localLoader.fetchResources(); loader.fetchResources(); } @@ -571,6 +571,7 @@ function initSearchInternal() { }; LI_HEIGHT = options.li_height?.() ?? defaultOptions.li_height; + console.log("LI_HEIGHT", LI_HEIGHT) BATCH_SIZE = options.batch_size ?? defaultOptions.batch_size; function resultsViewBuilder(options: InitSearchOptions) { if (options.listElemCreationFunc) { diff --git a/public/ts/multiselect.civet b/public/ts/multiselect.civet deleted file mode 100644 index 20dc647..0000000 --- a/public/ts/multiselect.civet +++ /dev/null @@ -1,118 +0,0 @@ -type FiberElement = HTMLElement & { - _fibermc_initialized?: boolean; -}; - -type MultiSelectValueElement = HTMLLabelElement & { - _fibermc_optionValue: TValue; - _fibermc_onSelect: () => void; - _fibermc_setChecked: (checked: boolean) => void; -}; - -type MultiSelectProps = Readonly<{ - rootElement: HTMLElement; - options: TValue[]; - setSelectedValues: ( - setValues: (currentValues: TValue[]) => TValue[] - ) => void; - currentValues: TValue[] | undefined; - renderValue: (val: TValue) => string; - key?: (val: TValue) => TKey; - leadingChildren?: HTMLElement[] -}>; - -function uniqueBy(arr: T[], key: (val: T) => TKey): T[] { - return [...new Map(arr.map((item) => [key(item), item])).values()]; -} - -export function initMultiselectElement({ - rootElement: _rootElement, - options, - setSelectedValues, - currentValues, - renderValue, - key, - leadingChildren = [] -}: MultiSelectProps): void { - leadingChildren ??= []; - const root = _rootElement as FiberElement; - const equals = (a: TValue, b: TValue) => (key ? key(a) == key(b) : a == b) - - const toggleValue = (val: TValue, curValues: TValue[]) => - curValues.some((el) => equals(val, el)) - ? curValues.filter((el) => !equals(val, el)) - : [...curValues, val] - - _rootElement.style.maxHeight = "60vh"; - _rootElement.style.overflow = "auto"; - - for el of leadingChildren - root.appendChild el; - - function getOptionElements() { - return [..._rootElement.children] - .slice(leadingChildren.length) as MultiSelectValueElement[]; - } - - const optionElements = options - .map((optionValue) => { - const element = document.createElement( - "label" - ) as MultiSelectValueElement; - - const check = element.appendChild( - document.createElement("input") - ||> .type = "checkbox" - ); - - const labelTextSpan = element.appendChild( - document.createElement("span") - ||> .textContent = (renderValue(optionValue) ?? "unknown") - ); - - element._fibermc_optionValue = optionValue; - element._fibermc_setChecked = (checked: boolean) => - (check.checked = checked); - - //@ts-expect-error - element._fibermc_onSelect = (e) => { - let newValues: Array; - setSelectedValues((curValues) => { - newValues = toggleValue(optionValue, curValues); - if (key) { - newValues = uniqueBy(newValues, key); - } - return newValues; - }); - getOptionElements().forEach((el) => { - const isSelected = newValues.some((val) => - equals(val, el._fibermc_optionValue) - ); - el._fibermc_setChecked(isSelected); - }); - console.log(e); - }; - check.addEventListener("change", element._fibermc_onSelect); - - if (currentValues?.some((val) => equals(val, optionValue))) { - element._fibermc_setChecked(true); - } - - element.classList.add("button"); - return element; - }); - - for el of optionElements - root.appendChild el; -} - -export function MultiSelect({ - rootElement: _rootElement, - options, - setSelectedValues, - currentValues, -}: MultiSelectProps) { - const rootElement = _rootElement as FiberElement; - if (!rootElement._fibermc_initialized) { - rootElement._fibermc_initialized = true; - } -} From 021bba1eae9dd1aef2f215646c4e31781697cddf Mon Sep 17 00:00:00 2001 From: John Paul R Date: Sun, 2 Mar 2025 18:32:21 -0500 Subject: [PATCH 11/11] make pg loader a bit more optional and proper disable it for now --- public/ts/mod_search_logic.ts | 38 +++-------------------------- public/ts/sql_loader.ts | 46 ++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/public/ts/mod_search_logic.ts b/public/ts/mod_search_logic.ts index 17bac2c..be4158d 100644 --- a/public/ts/mod_search_logic.ts +++ b/public/ts/mod_search_logic.ts @@ -12,7 +12,7 @@ import { MOD_DATA } from "./search_page_state" import { PGlite } from "@electric-sql/pglite"; import { effect } from "./effect.js"; import { CATEGORIES, initCategoriesSidebar, BoolMode, getSelectedCategoryIds, updateFilteredCategoryModCounts } from "./initCategoriesSidebar.js"; -import { getDbModData, updateDatabase } from "./sql_loader.js"; +import { createLocalLoader, getDbModData, updateDatabase } from "./sql_loader.js"; import { filterByVersion, initVersionsMultiSelect, SELECTED_VERSIONS } from "./versions_multiselect.js"; let prevTime = performance.now(); @@ -22,9 +22,6 @@ function logtime(message: string) { } logtime("start file"); -const db = new PGlite("idb://fibermc"); -await db.waitReady; -logtime("Database Ready!"); export { init, @@ -61,32 +58,7 @@ const apiUrl = `https://${ "dev.fibermc.com" }/api/v1.0`; -var localLoader = new AsyncDataResourceLoader({ - completionWaitForDCL: true, -}) - .addResourceFn(async () => { - logtime("Start local db query"); - if (!db) { - return undefined; - } - console.log("HAVE DB"); - - const temp_mod_data = await getDbModData(db, 100); - logtime("Start local db query...done!"); - - return temp_mod_data; - }, [ - async (temp_mod_data) => { - if (!temp_mod_data) { - return; - } - console.log("SET ROWS", temp_mod_data); - - setModData(temp_mod_data); - mod_data.sort((a, b) => b.downloadCount - a.downloadCount); - }, - ]); - +const localLoader = createLocalLoader(logtime) configureModsLoader(localLoader); var categoriesLoader = new AsyncDataResourceLoader({ @@ -117,11 +89,7 @@ var loader = new AsyncDataResourceLoader({ console.log("mod_data", mod_data); console.log("TABLE CREATED IF NEEDED"); - await updateDatabase(db, mod_data) - console.log( - "DONE SETUP", - await db.query("SELECT * FROM mod_data;") - ); + await updateDatabase(mod_data) }, ]); configureModsLoader(loader); diff --git a/public/ts/sql_loader.ts b/public/ts/sql_loader.ts index 502e626..c4ed29c 100644 --- a/public/ts/sql_loader.ts +++ b/public/ts/sql_loader.ts @@ -1,8 +1,16 @@ import { PGlite, Transaction } from "@electric-sql/pglite"; import { Mod } from "./mod_types"; +import { AsyncDataResourceLoader } from "./resource_loader"; +import { mod_data, setModData } from "./mod_search_logic"; + +const USE_LOCAL_LOADER = false; +const db = USE_LOCAL_LOADER ? new PGlite("idb://fibermc") : undefined; // Process large data sets without freezing the UI -async function processBatchesNonBlocking(db: PGlite, mod_data: Mod[]) { +async function processBatchesNonBlocking(mod_data: Mod[]) { + if (!db) { + return; + } const BATCH_SIZE = 500; const totalBatches = Math.ceil(mod_data.length / BATCH_SIZE); @@ -131,7 +139,10 @@ async function processSingleBatch(tx: Transaction, batch: Mod[]) { await tx.query(query, params); } -export async function updateDatabase(db: PGlite, mod_data: Mod[]) { +export async function updateDatabase(mod_data: Mod[]) { + if (!db) { + return; + } try { setLoadingState(true); await db.exec(` @@ -155,7 +166,7 @@ export async function updateDatabase(db: PGlite, mod_data: Mod[]) { ); `); - await processBatchesNonBlocking(db, mod_data); + await processBatchesNonBlocking(mod_data); } catch (error) { console.error("Error updating database:", error); } finally { @@ -191,3 +202,32 @@ export async function getDbModData(db: PGlite, count: number): Promise { ); return temp_mod_data.rows as Mod[]; } + +export function createLocalLoader(logtime: (msg: string) => void) { + return new AsyncDataResourceLoader({ + completionWaitForDCL: true, + }).addResourceFn(async () => { + logtime("Start local db query"); + if (!db) { + return undefined; + } + console.log("HAVE DB"); + await db.waitReady; + logtime("Database Ready!"); + + const temp_mod_data = await getDbModData(db, 100); + logtime("Start local db query...done!"); + + return temp_mod_data; + }, [ + async (temp_mod_data) => { + if (!temp_mod_data) { + return; + } + console.log("SET ROWS", temp_mod_data); + + setModData(temp_mod_data); + mod_data.sort((a, b) => b.downloadCount - a.downloadCount); + }, + ]); +}