From 15879a4df301fcbd9acc7904efc6288333201c48 Mon Sep 17 00:00:00 2001 From: User Date: Wed, 13 Aug 2025 20:49:24 -0400 Subject: [PATCH 1/8] add search dropdown to nav --- app/components/navbar.gjs | 10 +-- app/components/search/navbar-search.gjs | 72 ++++++++++++++++ app/routes/application.js | 5 ++ app/services/all-printings.js | 108 ++++++++++++++++++++++++ app/storages/all-printings.js | 15 ++++ app/styles/app.css | 37 ++++++++ config/environment.js | 4 +- package.json | 1 + pnpm-lock.yaml | 50 +++++++++++ 9 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 app/components/search/navbar-search.gjs create mode 100644 app/services/all-printings.js create mode 100644 app/storages/all-printings.js diff --git a/app/components/navbar.gjs b/app/components/navbar.gjs index ee11ad7..da217f5 100644 --- a/app/components/navbar.gjs +++ b/app/components/navbar.gjs @@ -2,6 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { LinkTo } from '@ember/routing'; +import NavbarSearch from 'netrunnerdb/components/search/navbar-search'; import { on } from '@ember/modifier'; import FaIcon from '@fortawesome/ember-fontawesome/components/fa-icon'; import { @@ -120,14 +121,7 @@ class Navbar extends Component {
-
- -
+
diff --git a/app/components/search/navbar-search.gjs b/app/components/search/navbar-search.gjs new file mode 100644 index 0000000..f0c1d5d --- /dev/null +++ b/app/components/search/navbar-search.gjs @@ -0,0 +1,72 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import { LinkTo } from '@ember/routing'; +import { on } from '@ember/modifier'; +import { and } from 'netrunnerdb/utils/template-operators'; + +export default class NavbarSearchComponent extends Component { + @service('all-printings') allPrintings; + + @tracked isOpen = false; + @tracked query = ''; + @tracked results = []; + + @action open() { + this.isOpen = true; + } + + @action onKeyDown(e) { + if (e.key === 'Escape') { + this.isOpen = false; + } + } + + @action handleBlur(e) { + const next = e.relatedTarget; + if (next && document.querySelector('.search-results')?.contains(next)) { + e.preventDefault?.(); + return; + } + this.isOpen = false; + } + + @action updateQuery(e) { + this.query = e.target.value; + let q = this.query?.trim(); + if (!q) { + this.results = []; + return; + } + this.results = this.allPrintings.search(q, 5); + } + + +} diff --git a/app/routes/application.js b/app/routes/application.js index 2d38635..edfaa2b 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -4,9 +4,14 @@ import { service } from '@ember/service'; export default class ApplicationRoute extends Route { @service session; @service intl; + @service('all-printings') allPrintings; async beforeModel() { await this.session.setup(); this.intl.setLocale(['pt-PT', 'en-US']); } + + activate() { + this.allPrintings.loadPrintings(); + } } diff --git a/app/services/all-printings.js b/app/services/all-printings.js new file mode 100644 index 0000000..c1ea65c --- /dev/null +++ b/app/services/all-printings.js @@ -0,0 +1,108 @@ +import Service from '@ember/service'; +import { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { storageFor } from 'ember-local-storage'; + +export default class AllPrintingsService extends Service { + @service store; + + @tracked allPrintings = []; + #loadPromise = null; + @storageFor('all-printings') allPrintingsCache; + + async needsRefresh() { + try { + let resp = await fetch('https://api.netrunnerdb.com/api/v3/public/cards?sort=-updated_at&page[size]=1'); + if (!resp.ok) return true; + let data = await resp.json(); + let updated = data?.data?.[0]?.attributes?.updated_at; + if (!updated) return true; + let cachedRemote = this.allPrintingsCache?.get?.('remoteUpdatedAt'); + return !cachedRemote || new Date(updated) > new Date(cachedRemote); + } catch (e) { + console.error("error checking if needs refresh", e); + return true; + } + } + + async loadPrintings() { + if (this.#loadPromise) return this.#loadPromise; + + this.#loadPromise = (async () => { + try { + + // 1) Load cached compact index if available and not stale + let cached = this.allPrintingsCache?.get?.('items') || []; + let stale = await this.needsRefresh(); + if (!stale && cached.length > 0) { + this.allPrintings = cached; + return; + } + + // 2) Fetch and persist compact index + let printings = await this.store.query('printing', { page: { size: 5000 } }); + let compact = []; + if (printings && typeof printings.forEach === 'function') { + printings.forEach((p) => { + compact.push({ + id: p.id, + title: p.title, + displaySubtypes: p.displaySubtypes, + factionId: p.factionId, + cardTypeId: p.cardTypeId, + }); + }); + } + this.allPrintings = compact; + this.allPrintingsCache?.set?.('items', compact); + this.allPrintingsCache?.set?.('updatedAt', new Date().toISOString()); + + try { + let resp = await fetch('https://api.netrunnerdb.com/api/v3/public/cards?sort=-updated_at&page[size]=1'); + if (resp.ok) { + let data = await resp.json(); + let updated = data?.data?.[0]?.attributes?.updated_at; + if (updated) this.allPrintingsCache?.set?.('remoteUpdatedAt', updated); + } + } catch (e) { + console.error("error fetching remote updated at", e); + } + } catch (e) { + console.error("error loading printings", e); + this.allPrintings = []; + } finally { + } + })(); + + return this.#loadPromise; + } + + normalize(str) { + return str.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ''); + } + + filterLocal(query) { + let q = this.normalize(query || ''); + if (!q) return []; + + const matches = this.allPrintings.filter((p) => { + let title = this.normalize(p.title); + return title.includes(q); + }); + + // matches that start with the query are prioritized + return matches.sort((a, b) => { + let aStartsWith = this.normalize(a.title).startsWith(q); + let bStartsWith = this.normalize(b.title).startsWith(q); + if (aStartsWith && !bStartsWith) return -1; + if (!aStartsWith && bStartsWith) return 1; + return 0; + }); + } + + search(query, limit) { + return this.filterLocal(query).slice(0, limit); + } +} + + diff --git a/app/storages/all-printings.js b/app/storages/all-printings.js new file mode 100644 index 0000000..04b3fa9 --- /dev/null +++ b/app/storages/all-printings.js @@ -0,0 +1,15 @@ +import StorageObject from 'ember-local-storage/local/object'; + +const AllPrintingsStorage = class extends StorageObject {}; + +AllPrintingsStorage.reopenClass({ + initialState() { + return { + items: [], + remoteUpdatedAt: null, + updatedAt: null, + }; + }, +}); + +export default AllPrintingsStorage; diff --git a/app/styles/app.css b/app/styles/app.css index fddf7ed..4831a4f 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -1054,3 +1054,40 @@ table.results tr:nth-child(even) { .user-content p:not(:first-of-type) { margin-top: 1em; } + +/* SEARCH */ + +.search-results { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + background-color: var(--color--box-bg); + border: 1px solid var(--color--box-border); + border-radius: 6px; + box-shadow: 0 6px 16px rgb(0 0 0 / 25%); + max-height: 320px; + overflow-y: auto; + padding: 4px 0; + z-index: 1000; +} + +.search-container { + position: relative; +} + +.search-results > * { + padding: 6px 10px; +} + +.search-results a { + display: block; + padding: 6px 10px; + color: var(--color--default); + text-decoration: none; +} + +.search-results a:hover { + background-color: var(--color--secondary-bg); + text-decoration: none; +} diff --git a/config/environment.js b/config/environment.js index 184b772..d17da10 100644 --- a/config/environment.js +++ b/config/environment.js @@ -28,7 +28,7 @@ module.exports = function (environment) { }, // API_URL: 'http://localhost:3000/api/v3', - API_URL: 'https://api-preview.netrunnerdb.com/api/v3', + API_URL: 'https://api.netrunnerdb.com/api/v3', googleFonts: ['Merriweather Sans', 'Outfit', 'Inter', 'Nova Mono'], APP: { @@ -60,7 +60,7 @@ module.exports = function (environment) { } if (environment === 'production') { - ENV.API_URL = 'https://api-preview.netrunnerdb.com/api/v3'; + ENV.API_URL = 'https://api.netrunnerdb.com/api/v3'; ENV['ember-simple-auth-oidc'].clientId = 'nrdb-v2'; ENV['ember-simple-auth-oidc'].afterLogoutUri = 'https://v2.netrunnerdb.com/'; diff --git a/package.json b/package.json index 32cb174..fe2ced1 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "ember-inflector": "^6.0.0", "ember-intl": "^7.3.0", "ember-load-initializers": "^3.0.1", + "ember-local-storage": "^2.0.7", "ember-math-helpers": "^5.0.0", "ember-modifier": "^4.2.2", "ember-page-title": "9.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dc9fb4..65e1279 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: ember-load-initializers: specifier: ^3.0.1 version: 3.0.1(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)) + ember-local-storage: + specifier: ^2.0.7 + version: 2.0.7(@babel/core@7.27.4) ember-math-helpers: specifier: ^5.0.0 version: 5.0.0(@babel/core@7.27.4)(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)) @@ -1964,6 +1967,9 @@ packages: blank-object@1.0.2: resolution: {integrity: sha512-kXQ19Xhoghiyw66CUiGypnuRpWlbHAzY/+NyvqTEdTfhfQGH1/dbEMYiXju7fYKIFePpzp/y9dsu5Cu/PkmawQ==} + blob-polyfill@7.0.20220408: + resolution: {integrity: sha512-oD8Ydw+5lNoqq+en24iuPt1QixdPpe/nUF8azTHnviCZYu9zUC+TwdzIp5orpblJosNlgNbVmmAb//c6d6ImUQ==} + bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} @@ -3051,6 +3057,10 @@ packages: peerDependencies: ember-source: '>=4.0' + ember-copy@2.0.1: + resolution: {integrity: sha512-N/XFvZszrzyyX4IcNoeK4mJvIItNuONumhPLqi64T8NDjJkxBj4Pq61rvMkJx/9eZ8alzE4I8vYKOLxT0FvRuQ==} + engines: {node: 10.* || >= 12} + ember-data@5.5.0: resolution: {integrity: sha512-qAuVKeCXn4tiqON9orbjS7H3iitCw5GC+XGdbqRk4Ow2phn/RRsCWf98KJLtB8tmflyp4l3Q1o4nJwjsNeDpeQ==} engines: {node: '>= 18.20.8'} @@ -3067,6 +3077,10 @@ packages: qunit: optional: true + ember-destroyable-polyfill@2.0.3: + resolution: {integrity: sha512-TovtNqCumzyAiW0/OisSkkVK93xnVF4NRU6+FN0ubpfwEOpRrmM2RqDwXI6YAChCgSHON1cz0DfQStpA1Gjuuw==} + engines: {node: 10.* || >= 12} + ember-element-helper@0.8.8: resolution: {integrity: sha512-3slTltQV5ke53t3YVP2GYoswsQ6y+lhuVzKmt09tbEx91DapG8I/xa8W5OA0StvcQlavL3/vHrz/vCQEFs8bBA==} engines: {node: 14.* || 16.* || >= 18} @@ -3131,6 +3145,10 @@ packages: peerDependencies: ember-source: '>= 5' + ember-local-storage@2.0.7: + resolution: {integrity: sha512-EPvxH/27mIzrX/EEgng+FG6HD0ri/God9OH/9mhmgPSXrgMNq/614Z3NMnooM4QKIEBAvr0p+p1UL2DgrTTMNg==} + engines: {node: 12.* || 14.* || >= 16} + ember-math-helpers@5.0.0: resolution: {integrity: sha512-UKChQuu1Ki57NGMFF0V1mbRJ5LtkZ+EMIdCl5w+3nrwCOzn8GpePQDgtQqgdE3tFrm3TsHfLgHtfa38uNSSG6w==} engines: {node: '>= 18'} @@ -8838,6 +8856,8 @@ snapshots: blank-object@1.0.2: {} + blob-polyfill@7.0.20220408: {} + bluebird@3.7.2: {} body-parser@1.20.3: @@ -10478,6 +10498,12 @@ snapshots: transitivePeerDependencies: - supports-color + ember-copy@2.0.1: + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + ember-data@5.5.0(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.4))(@ember/test-waiters@4.1.0)(ember-inflector@6.0.0(@babel/core@7.27.4))(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5))(qunit@2.24.1): dependencies: '@ember-data/adapter': 5.5.0(@ember-data/legacy-compat@5.5.0(0114dc230c5492a332c39e2c4fb58d69))(@ember-data/request-utils@5.5.0(@ember-data/request@5.5.0(@ember/test-waiters@4.1.0)(@warp-drive/core-types@5.5.0))(@ember/string@3.1.1)(@warp-drive/core-types@5.5.0)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@5.5.0(@ember-data/request-utils@5.5.0(@ember-data/request@5.5.0(@ember/test-waiters@4.1.0)(@warp-drive/core-types@5.5.0))(@ember/string@3.1.1)(@warp-drive/core-types@5.5.0)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@5.5.0(@ember/test-waiters@4.1.0)(@warp-drive/core-types@5.5.0))(@ember-data/tracking@5.5.0(@warp-drive/core-types@5.5.0)(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)))(@warp-drive/core-types@5.5.0)(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)))(@warp-drive/core-types@5.5.0)(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)) @@ -10508,6 +10534,15 @@ snapshots: - ember-provide-consume-context - supports-color + ember-destroyable-polyfill@2.0.3(@babel/core@7.27.4): + dependencies: + ember-cli-babel: 7.26.11 + ember-cli-version-checker: 5.1.2 + ember-compatibility-helpers: 1.2.7(@babel/core@7.27.4) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-element-helper@0.8.8: dependencies: '@embroider/addon-shim': 1.10.0 @@ -10619,6 +10654,21 @@ snapshots: dependencies: ember-source: 6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5) + ember-local-storage@2.0.7(@babel/core@7.27.4): + dependencies: + blob-polyfill: 7.0.20220408 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + broccoli-stew: 3.0.0 + ember-cli-babel: 7.26.11 + ember-cli-string-utils: 1.1.0 + ember-cli-version-checker: 5.1.2 + ember-copy: 2.0.1 + ember-destroyable-polyfill: 2.0.3(@babel/core@7.27.4) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-math-helpers@5.0.0(@babel/core@7.27.4)(ember-source@6.5.0(@glimmer/component@2.0.0)(rsvp@4.8.5)): dependencies: '@embroider/addon-shim': 1.10.0 From c92a507816067d4b05ec211210dba62f2b118ac4 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 08:04:43 -0400 Subject: [PATCH 2/8] use store for remote updated_at and some keyboard interactions --- app/components/search/navbar-search.gjs | 1 + app/services/all-printings.js | 36 ++++++++++++++----------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/components/search/navbar-search.gjs b/app/components/search/navbar-search.gjs index f0c1d5d..f5e569f 100644 --- a/app/components/search/navbar-search.gjs +++ b/app/components/search/navbar-search.gjs @@ -33,6 +33,7 @@ export default class NavbarSearchComponent extends Component { } @action updateQuery(e) { + this.isOpen = true; this.query = e.target.value; let q = this.query?.trim(); if (!q) { diff --git a/app/services/all-printings.js b/app/services/all-printings.js index c1ea65c..aac964b 100644 --- a/app/services/all-printings.js +++ b/app/services/all-printings.js @@ -10,12 +10,23 @@ export default class AllPrintingsService extends Service { #loadPromise = null; @storageFor('all-printings') allPrintingsCache; + async getRemoteUpdatedAt() { + try { + let cards = await this.store.query('card', { + sort: '-updated_at', + page: { size: 1 }, + fields: { cards: 'updated_at' }, + }); + let first = cards?.[0]; + return first?.updatedAt ?? null; + } catch (_e) { + return null; + } + } + async needsRefresh() { try { - let resp = await fetch('https://api.netrunnerdb.com/api/v3/public/cards?sort=-updated_at&page[size]=1'); - if (!resp.ok) return true; - let data = await resp.json(); - let updated = data?.data?.[0]?.attributes?.updated_at; + let updated = await this.getRemoteUpdatedAt(); if (!updated) return true; let cachedRemote = this.allPrintingsCache?.get?.('remoteUpdatedAt'); return !cachedRemote || new Date(updated) > new Date(cachedRemote); @@ -31,9 +42,11 @@ export default class AllPrintingsService extends Service { this.#loadPromise = (async () => { try { - // 1) Load cached compact index if available and not stale + // 1) Determine staleness + let remoteUpdatedAt = await this.getRemoteUpdatedAt(); let cached = this.allPrintingsCache?.get?.('items') || []; - let stale = await this.needsRefresh(); + let cachedRemote = this.allPrintingsCache?.get?.('remoteUpdatedAt'); + let stale = !remoteUpdatedAt || !cachedRemote || new Date(remoteUpdatedAt) > new Date(cachedRemote); if (!stale && cached.length > 0) { this.allPrintings = cached; return; @@ -57,15 +70,8 @@ export default class AllPrintingsService extends Service { this.allPrintingsCache?.set?.('items', compact); this.allPrintingsCache?.set?.('updatedAt', new Date().toISOString()); - try { - let resp = await fetch('https://api.netrunnerdb.com/api/v3/public/cards?sort=-updated_at&page[size]=1'); - if (resp.ok) { - let data = await resp.json(); - let updated = data?.data?.[0]?.attributes?.updated_at; - if (updated) this.allPrintingsCache?.set?.('remoteUpdatedAt', updated); - } - } catch (e) { - console.error("error fetching remote updated at", e); + if (remoteUpdatedAt) { + this.allPrintingsCache?.set?.('remoteUpdatedAt', remoteUpdatedAt); } } catch (e) { console.error("error loading printings", e); From e056c840c69e8a27d36c9cfbf3cbb6b23d4c5dd2 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 09:11:27 -0400 Subject: [PATCH 3/8] default to cache before invalidating --- app/services/all-printings.js | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/app/services/all-printings.js b/app/services/all-printings.js index aac964b..04bcf2f 100644 --- a/app/services/all-printings.js +++ b/app/services/all-printings.js @@ -41,31 +41,31 @@ export default class AllPrintingsService extends Service { this.#loadPromise = (async () => { try { + // 1) Load from cache first if available + let cached = this.allPrintingsCache?.get?.('items') || []; + if (cached.length > 0) { + this.allPrintings = cached; + } - // 1) Determine staleness + // 2) Check freshness in background let remoteUpdatedAt = await this.getRemoteUpdatedAt(); - let cached = this.allPrintingsCache?.get?.('items') || []; let cachedRemote = this.allPrintingsCache?.get?.('remoteUpdatedAt'); let stale = !remoteUpdatedAt || !cachedRemote || new Date(remoteUpdatedAt) > new Date(cachedRemote); + + // 3) If data is fresh or we have no cached data, return early if (!stale && cached.length > 0) { - this.allPrintings = cached; return; } - // 2) Fetch and persist compact index + // 4) Fetch and persist fresh data if stale or no cache let printings = await this.store.query('printing', { page: { size: 5000 } }); - let compact = []; - if (printings && typeof printings.forEach === 'function') { - printings.forEach((p) => { - compact.push({ + let compact = printings.map((p) => { + return { id: p.id, title: p.title, - displaySubtypes: p.displaySubtypes, - factionId: p.factionId, - cardTypeId: p.cardTypeId, - }); - }); - } + }; + }); + this.allPrintings = compact; this.allPrintingsCache?.set?.('items', compact); this.allPrintingsCache?.set?.('updatedAt', new Date().toISOString()); @@ -75,7 +75,6 @@ export default class AllPrintingsService extends Service { } } catch (e) { console.error("error loading printings", e); - this.allPrintings = []; } finally { } })(); From 63911fe72d614a24845cc0c7fb78adb3d18836b7 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 09:22:02 -0400 Subject: [PATCH 4/8] add compression --- app/routes/application.js | 10 +++++- app/services/all-printings.js | 61 +++++++++++++++++++++++++++++------ package.json | 5 ++- pnpm-lock.yaml | 10 ++++++ 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/app/routes/application.js b/app/routes/application.js index edfaa2b..b75de0e 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -6,12 +6,20 @@ export default class ApplicationRoute extends Route { @service intl; @service('all-printings') allPrintings; + queryParams = { + refreshCache: { refreshModel: false } + }; + async beforeModel() { await this.session.setup(); this.intl.setLocale(['pt-PT', 'en-US']); } activate() { - this.allPrintings.loadPrintings(); + // Check if cache refresh is requested via query parameter + const params = this.paramsFor('application'); + const forceRefresh = params.refreshCache === 'true'; + + this.allPrintings.loadPrintings(forceRefresh); } } diff --git a/app/services/all-printings.js b/app/services/all-printings.js index 04bcf2f..e8c4416 100644 --- a/app/services/all-printings.js +++ b/app/services/all-printings.js @@ -2,6 +2,7 @@ import Service from '@ember/service'; import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { storageFor } from 'ember-local-storage'; +import LZString from 'lz-string'; export default class AllPrintingsService extends Service { @service store; @@ -10,6 +11,41 @@ export default class AllPrintingsService extends Service { #loadPromise = null; @storageFor('all-printings') allPrintingsCache; + // Compression helpers + compressData(data) { + try { + const jsonString = JSON.stringify(data); + return LZString.compress(jsonString); + } catch (e) { + console.error('Error compressing data:', e); + return null; + } + } + + decompressData(compressedData) { + try { + if (!compressedData) return null; + const decompressed = LZString.decompress(compressedData); + return decompressed ? JSON.parse(decompressed) : null; + } catch (e) { + console.error('Error decompressing data:', e); + return null; + } + } + + // Enhanced cache methods with compression + getCachedItems() { + const compressed = this.allPrintingsCache?.get?.('items'); + return this.decompressData(compressed) || []; + } + + setCachedItems(items) { + const compressed = this.compressData(items); + if (compressed) { + this.allPrintingsCache?.set?.('items', compressed); + } + } + async getRemoteUpdatedAt() { try { let cards = await this.store.query('card', { @@ -36,23 +72,28 @@ export default class AllPrintingsService extends Service { } } - async loadPrintings() { + async loadPrintings(forceRefresh = false) { + // Reset load promise if forcing refresh + if (forceRefresh) { + this.#loadPromise = null; + } + if (this.#loadPromise) return this.#loadPromise; this.#loadPromise = (async () => { try { - // 1) Load from cache first if available - let cached = this.allPrintingsCache?.get?.('items') || []; - if (cached.length > 0) { + // 1) Load from cache first if available (unless forcing refresh) + let cached = this.getCachedItems(); + if (cached.length > 0 && !forceRefresh) { this.allPrintings = cached; } // 2) Check freshness in background let remoteUpdatedAt = await this.getRemoteUpdatedAt(); let cachedRemote = this.allPrintingsCache?.get?.('remoteUpdatedAt'); - let stale = !remoteUpdatedAt || !cachedRemote || new Date(remoteUpdatedAt) > new Date(cachedRemote); + let stale = forceRefresh || !remoteUpdatedAt || !cachedRemote || new Date(remoteUpdatedAt) > new Date(cachedRemote); - // 3) If data is fresh or we have no cached data, return early + // 3) If data is fresh and not forcing refresh, return early if (!stale && cached.length > 0) { return; } @@ -60,14 +101,16 @@ export default class AllPrintingsService extends Service { // 4) Fetch and persist fresh data if stale or no cache let printings = await this.store.query('printing', { page: { size: 5000 } }); let compact = printings.map((p) => { + // Serialize all attributes and properties from the printing object + let serialized = p.serialize(); return { - id: p.id, - title: p.title, + id: p.id, + ...serialized.data.attributes }; }); this.allPrintings = compact; - this.allPrintingsCache?.set?.('items', compact); + this.setCachedItems(compact); this.allPrintingsCache?.set?.('updatedAt', new Date().toISOString()); if (remoteUpdatedAt) { diff --git a/package.json b/package.json index fe2ced1..b41582c 100644 --- a/package.json +++ b/package.json @@ -105,5 +105,8 @@ "ember": { "edition": "octane" }, - "packageManager": "pnpm@10.12.1" + "packageManager": "pnpm@10.12.1", + "dependencies": { + "lz-string": "^1.5.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65e1279..40a1f0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,10 @@ patchedDependencies: importers: .: + dependencies: + lz-string: + specifier: ^1.5.0 + version: 1.5.0 devDependencies: '@babel/core': specifier: ^7.27.1 @@ -4676,6 +4680,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.24.1: resolution: {integrity: sha512-YBfNxbJiixMzxW40XqJEIldzHyh5f7CZKalo1uZffevyrPEX8Qgo9s0dmcORLHdV47UyvJg8/zD+6hQG3qvJrA==} @@ -12642,6 +12650,8 @@ snapshots: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + magic-string@0.24.1: dependencies: sourcemap-codec: 1.4.8 From 7e5de98a9aa55294a5cb09d49f8fcb5fbfe7e711 Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 09:27:40 -0400 Subject: [PATCH 5/8] fix lz-string dependency --- package.json | 6 ++---- pnpm-lock.yaml | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b41582c..f43e17a 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "eslint-plugin-qunit": "^8.1.2", "globals": "^16.2.0", "loader.js": "^4.7.0", + "lz-string": "^1.5.0", "markdown-it": "^14.1.0", "prettier": "^3.3.3", "prettier-plugin-ember-template-tag": "^2.0.6", @@ -105,8 +106,5 @@ "ember": { "edition": "octane" }, - "packageManager": "pnpm@10.12.1", - "dependencies": { - "lz-string": "^1.5.0" - } + "packageManager": "pnpm@10.12.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40a1f0d..6a21474 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,10 +15,6 @@ patchedDependencies: importers: .: - dependencies: - lz-string: - specifier: ^1.5.0 - version: 1.5.0 devDependencies: '@babel/core': specifier: ^7.27.1 @@ -200,6 +196,9 @@ importers: loader.js: specifier: ^4.7.0 version: 4.7.0 + lz-string: + specifier: ^1.5.0 + version: 1.5.0 markdown-it: specifier: ^14.1.0 version: 14.1.0 From 7cff8adb20eb560ce9d88905dd30358fa33b9c2c Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 09:43:08 -0400 Subject: [PATCH 6/8] revert change to API url --- config/environment.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/environment.js b/config/environment.js index d17da10..184b772 100644 --- a/config/environment.js +++ b/config/environment.js @@ -28,7 +28,7 @@ module.exports = function (environment) { }, // API_URL: 'http://localhost:3000/api/v3', - API_URL: 'https://api.netrunnerdb.com/api/v3', + API_URL: 'https://api-preview.netrunnerdb.com/api/v3', googleFonts: ['Merriweather Sans', 'Outfit', 'Inter', 'Nova Mono'], APP: { @@ -60,7 +60,7 @@ module.exports = function (environment) { } if (environment === 'production') { - ENV.API_URL = 'https://api.netrunnerdb.com/api/v3'; + ENV.API_URL = 'https://api-preview.netrunnerdb.com/api/v3'; ENV['ember-simple-auth-oidc'].clientId = 'nrdb-v2'; ENV['ember-simple-auth-oidc'].afterLogoutUri = 'https://v2.netrunnerdb.com/'; From 8440b1850dcc378d955d66aedc8e5dc89c65186c Mon Sep 17 00:00:00 2001 From: User Date: Thu, 14 Aug 2025 09:46:30 -0400 Subject: [PATCH 7/8] fix lint --- app/services/all-printings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/all-printings.js b/app/services/all-printings.js index e8c4416..5c3f0e0 100644 --- a/app/services/all-printings.js +++ b/app/services/all-printings.js @@ -55,7 +55,8 @@ export default class AllPrintingsService extends Service { }); let first = cards?.[0]; return first?.updatedAt ?? null; - } catch (_e) { + } catch (e) { + console.error("error getting remote updated at", e); return null; } } @@ -118,7 +119,6 @@ export default class AllPrintingsService extends Service { } } catch (e) { console.error("error loading printings", e); - } finally { } })(); From 8bb4267acd494927248d9e7f01bea80076beed17 Mon Sep 17 00:00:00 2001 From: User Date: Mon, 18 Aug 2025 11:17:02 -0400 Subject: [PATCH 8/8] Fix lint --- app/styles/app.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/styles/app.css b/app/styles/app.css index ec3e0dd..faab975 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -671,6 +671,18 @@ table.game-card-stats tr:nth-child(even) { background-color: var(--color--header-bg); } +.search-results a { + display: block; + padding: 6px 10px; + color: var(--color--default); + text-decoration: none; +} + +.search-results a:hover { + background-color: var(--color--secondary-bg); + text-decoration: none; +} + #bottom-nav { top: 0; position: sticky; @@ -1082,15 +1094,3 @@ table.results tr:nth-child(even) { .search-results > * { padding: 6px 10px; } - -.search-results a { - display: block; - padding: 6px 10px; - color: var(--color--default); - text-decoration: none; -} - -.search-results a:hover { - background-color: var(--color--secondary-bg); - text-decoration: none; -}