From 7344a305834f96b0e6a62adbc379dac5a733ae0b Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:01:42 -0400 Subject: [PATCH 01/24] Add Deflock Store to "hang signs" linkon the website (#116) * Add the store code from agora.markets Uploads a stylesheet and store code to popluate the deflock store hosted by agora.markets * fix store --------- Co-authored-by: Will Freeman --- webapp/bun.lock | 3 + webapp/src/App.vue | 2 +- webapp/src/views/Store.vue | 546 ++++++++++++++++++++++++------------- 3 files changed, 353 insertions(+), 198 deletions(-) diff --git a/webapp/bun.lock b/webapp/bun.lock index 2928984..256070a 100644 --- a/webapp/bun.lock +++ b/webapp/bun.lock @@ -14,6 +14,7 @@ "pinia": "^2.3.0", "vue": "^3.4.29", "vue-router": "^4.3.3", + "vue-turnstile": "^1.0.11", "vuetify": "^3.7.2", }, "devDependencies": { @@ -344,6 +345,8 @@ "vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": "bin/vue-tsc.js" }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], + "vue-turnstile": ["vue-turnstile@1.0.11", "", { "peerDependencies": { "vue": "^3.2.45" } }, "sha512-iaTBoZ5oUqtNRto6bmbn6FQvW0h/sK7mPUJc1Qn4em+cELXN59U2FQTcpWfKssV3OY6lEZzmCpcn/zrb7htK3A=="], + "vuetify": ["vuetify@3.11.2", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-1lL0qN6JIdbx6xGYpo6dnx378EfC0t4EotPJdP4go8ThmIdRO3xLva1ALxhxi5lSYTht4R9OVk9miVnwVfDx3A=="], "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], diff --git a/webapp/src/App.vue b/webapp/src/App.vue index c5f9c01..ead38f3 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -42,7 +42,7 @@ const items = [ const contributeItems = [ { title: 'Submit Cameras', icon: 'mdi-map-marker-plus', to: '/report' }, - { title: 'Hang Signs', icon: 'mdi-sign-direction', to: '/store' }, + { title: 'Store', icon: 'mdi-shopping', to: '/store' }, { title: 'Public Records', icon: 'mdi-file-document', to: '/foia' }, { title: 'City Council', icon: 'mdi-account-voice', to: '/council' }, ] diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index e1d3181..d24ec15 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -2,20 +2,65 @@ - -
- -

Loading printables...

+ +

Shop

+

Physical goods shipped to your door.

+ + +
+ + ALL + + + + + + + +
+ + +
+
+
+ + + + +
+ +

Loading printables...

- Failed to load printables: {{ error }} -

Printables

-

- Signs, stickers, zines, and more! -

- - -
- - - - - - - - -
+

Signs, stickers, zines, and more!

+ + + + + + + + + - - -
- - - -
+ + + + - -

- {{ printable.title }} -

+

{{ printable.title }}

- {{ getTypeIcon(printable.type) }} {{ deCamel(printable.type) }} - -
+ +
mdi-account by {{ printable.author }} - - - \ No newline at end of file + \ No newline at end of file From d87b6e93dedaf6bf7b5794e3d1365c221ccbc4bc Mon Sep 17 00:00:00 2001 From: Will Freeman Date: Wed, 17 Jun 2026 18:34:20 -0600 Subject: [PATCH 02/24] store upgrades --- webapp/src/views/Store.vue | 495 +++++++++++++++++++++---------------- 1 file changed, 283 insertions(+), 212 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index d24ec15..8640b1f 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -9,211 +9,233 @@ - -

Shop

-

Physical goods shipped to your door.

- - -
- - ALL - - - - - - - -
- - -
-
-
- - - - -
- -

Loading printables...

-
- - - Failed to load printables: {{ error }} - + Shop + Printables + -
-

Printables

-

Signs, stickers, zines, and more!

+ - - - - - - - - - - - - - + + + + - From 8ac38e1a6e57532a42ca562d903cdda91bc2c1ca Mon Sep 17 00:00:00 2001 From: Will Freeman Date: Wed, 17 Jun 2026 19:10:43 -0600 Subject: [PATCH 07/24] printables -> downloads --- webapp/src/App.vue | 2 +- webapp/src/views/Store.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/App.vue b/webapp/src/App.vue index a8a93ed..0b35159 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -44,6 +44,7 @@ const contributeItems = [ { title: 'Submit Cameras', icon: 'mdi-map-marker-plus', to: '/report' }, { title: 'Public Records', icon: 'mdi-file-document', to: '/foia' }, { title: 'City Council', icon: 'mdi-account-voice', to: '/council' }, + { title: 'Support Us', icon: 'mdi-cash-multiple', to: '/donate' }, ] const metaItems = [ @@ -51,7 +52,6 @@ const metaItems = [ { title: 'Local Groups', icon: 'mdi-account-group', to: '/groups' }, { title: 'Contact', icon: 'mdi-email-outline', to: '/contact' }, { title: 'GitHub', icon: 'mdi-github', href: 'https://github.com/frillweeman/deflock'}, - { title: 'Support Us', icon: 'mdi-cash-multiple', to: '/donate'}, ]; const drawer = ref(false) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 1967f86..2a54804 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -16,7 +16,7 @@ class="mb-6" > Shop - Printables + Downloads From 2bd522fb92b5df857fa702935f4bd9960676072b Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:41:56 -0400 Subject: [PATCH 08/24] Update Store.vue Added a specific collection entry for the NOALPRS.COM Collection --- webapp/src/views/Store.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 2a54804..f2e8fc6 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -268,7 +268,8 @@ const COLLECTIONS: Record> = { 'Tech Accessories': { 'Phone Cases': '519708147994', 'Laptop Sleeves': '519708213530', - }, + }, + 'NO ALPRS': '520528560410' }; const SHOPIFY_OPTIONS = { @@ -560,4 +561,4 @@ watch(shopifyContainer, (el) => { onMounted(() => { fetchPrintables(); }); - \ No newline at end of file + From 748357886e358323a59d44ac3d74823c370e312f Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:44:21 -0400 Subject: [PATCH 09/24] Update Store.vue --- webapp/src/views/Store.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index f2e8fc6..0eb49d5 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -269,7 +269,7 @@ const COLLECTIONS: Record> = { 'Phone Cases': '519708147994', 'Laptop Sleeves': '519708213530', }, - 'NO ALPRS': '520528560410' + 'NO ALPRS': {'520528560410'}, }; const SHOPIFY_OPTIONS = { From d7517192dbf17f409a28632a0bd0236724651115 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:46:45 -0400 Subject: [PATCH 10/24] Update Store.vue --- webapp/src/views/Store.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 0eb49d5..0cd0d96 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -269,7 +269,8 @@ const COLLECTIONS: Record> = { 'Phone Cases': '519708147994', 'Laptop Sleeves': '519708213530', }, - 'NO ALPRS': {'520528560410'}, + 'Featured': { + 'No ALPRs': '520528560410'}, }; const SHOPIFY_OPTIONS = { From 517d6b054bf5353e5b83cfb59c19c666f41a6d72 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:48:01 -0400 Subject: [PATCH 11/24] Update Store.vue Changed to a 3 wide design to match the PRintables --- webapp/src/views/Store.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 0cd0d96..a8950db 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -262,7 +262,7 @@ const COLLECTIONS: Record> = { 'Yard Signs': '519582056730', }, 'Car Accessories': { - Magnets: '519582089498', + 'Magnets': '519582089498', 'License Plates': '519708410138', }, 'Tech Accessories': { @@ -278,7 +278,7 @@ const SHOPIFY_OPTIONS = { styles: { product: { '@media (min-width: 601px)': { - 'max-width': 'calc(25% - 20px)', + 'max-width': 'calc(33% - 20px)', 'margin-left': '20px', 'margin-bottom': '50px', width: 'calc(25% - 20px)', From 2b4f6c64d650e1368c5b86d73be3a290abbcefef Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:49:09 -0400 Subject: [PATCH 12/24] Update Store.vue --- webapp/src/views/Store.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index a8950db..ab1621e 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -281,7 +281,7 @@ const SHOPIFY_OPTIONS = { 'max-width': 'calc(33% - 20px)', 'margin-left': '20px', 'margin-bottom': '50px', - width: 'calc(25% - 20px)', + width: 'calc(33% - 20px)', }, }, title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, From 441b487f35c6e3e95285db62ee94d3bb0f78ea95 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:50:40 -0400 Subject: [PATCH 13/24] Attempting to theme the shopify elements like the vie elements --- webapp/src/views/Store.vue | 292 +++++++++++++++++++++---------------- 1 file changed, 168 insertions(+), 124 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index ab1621e..4a3c4c0 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -23,7 +23,8 @@ - + + -
+

+ T-shirts, stickers, yard signs, and more — proceeds support the anti-surveillance movement. +

+ + + + + + + + +
-
+
+ @@ -231,6 +254,7 @@ import { ref, onMounted, computed, watch } from 'vue'; import type { Ref } from 'vue'; import { useRoute, useRouter } from 'vue-router'; +import { useTheme } from 'vuetify'; import DefaultLayout from '@/layouts/DefaultLayout.vue'; import Hero from '@/components/layout/Hero.vue'; @@ -268,130 +292,144 @@ const COLLECTIONS: Record> = { 'Tech Accessories': { 'Phone Cases': '519708147994', 'Laptop Sleeves': '519708213530', - }, + }, 'Featured': { - 'No ALPRs': '520528560410'}, + 'No ALPRs': '520528560410', + }, }; -const SHOPIFY_OPTIONS = { - product: { - styles: { - product: { - '@media (min-width: 601px)': { - 'max-width': 'calc(33% - 20px)', - 'margin-left': '20px', - 'margin-bottom': '50px', - width: 'calc(33% - 20px)', +// Modal text uses a light-on-dark or dark-on-light palette depending on the +// active Vuetify theme. The product grid tiles always render on the SDK's own +// dark card surface so their text stays white regardless of theme. +function buildShopifyOptions(dark: boolean) { + const modalText = dark ? '#e0e0e0' : '#4c4c4c'; + const modalText2 = dark ? '#b0b0b0' : '#6b6b6b'; + + return { + product: { + styles: { + product: { + '@media (min-width: 601px)': { + 'max-width': 'calc(33% - 20px)', + 'margin-left': '20px', + 'margin-bottom': '50px', + width: 'calc(33% - 20px)', + }, + }, + title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, + button: { + 'font-family': 'Raleway, sans-serif', + 'font-weight': 'bold', + 'font-size': '16px', + 'padding-top': '16px', + 'padding-bottom': '16px', + ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, + 'background-color': 'rgb(18, 151, 195)', + color: '#ffffff', + ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, + 'border-radius': '4px', + 'padding-left': '20px', + 'padding-right': '20px', + }, + quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, + price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, + compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, + unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, + description: { 'font-family': 'Raleway, sans-serif' }, + }, + buttonDestination: 'modal', + contents: { button: false, options: false }, + isButton: true, + text: { button: 'View Item' }, + googleFonts: ['Raleway'], + }, + productSet: { + styles: { + products: { '@media (min-width: 601px)': { 'margin-left': '-20px' } }, + }, + text: { + nextPageButton: 'Load more', + }, + }, + modalProduct: { + contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true }, + styles: { + product: { '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' } }, + button: { + 'font-family': 'Raleway, sans-serif', + 'font-weight': 'bold', + 'font-size': '16px', + 'padding-top': '16px', + 'padding-bottom': '16px', + ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, + 'background-color': 'rgb(18, 151, 195)', + color: '#ffffff', + ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, + 'border-radius': '4px', + 'padding-left': '20px', + 'padding-right': '20px', + }, + quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, + title: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', 'font-size': '26px', color: modalText }, + price: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '18px', color: modalText }, + compareAt: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: modalText2 }, + unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: modalText2 }, + description: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '14px', color: modalText2 }, + }, + googleFonts: ['Raleway'], + text: { button: 'Add to cart' }, + }, + option: { + styles: { + label: { 'font-family': 'Raleway, sans-serif' }, + select: { 'font-family': 'Raleway, sans-serif' }, + }, + googleFonts: ['Raleway'], + }, + cart: { + styles: { + button: { + 'font-family': 'Raleway, sans-serif', + 'font-weight': 'bold', + 'font-size': '16px', + 'padding-top': '16px', + 'padding-bottom': '16px', + ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, + 'background-color': 'rgb(18, 151, 195)', + color: '#ffffff', + ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, + 'border-radius': '4px', }, }, - title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, - button: { - 'font-family': 'Raleway, sans-serif', - 'font-weight': 'bold', - 'font-size': '16px', - 'padding-top': '16px', - 'padding-bottom': '16px', - ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, - 'background-color': 'rgb(18, 151, 195)', - color: '#ffffff', - ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, - 'border-radius': '0px', - 'padding-left': '20px', - 'padding-right': '20px', + text: { + total: 'Subtotal', + notice: 'Shipping and discount codes are added at checkout - powered by Agora Markets', + button: 'Checkout', + noteDescription: 'Additional Information for the deflock.org team', }, - quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, - price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, - compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, - unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, - description: { 'font-family': 'Raleway, sans-serif' }, + googleFonts: ['Raleway'], }, - buttonDestination: 'modal', - contents: { button: false, options: false }, - isButton: true, - text: { button: 'View Item' }, - googleFonts: ['Raleway'], - }, - productSet: { - styles: { - products: { '@media (min-width: 601px)': { 'margin-left': '-20px' } }, - }, - text: { - nextPageButton: 'Load more', - }, - }, - modalProduct: { - contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true }, - styles: { - product: { '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' } }, - button: { - 'font-family': 'Raleway, sans-serif', - 'font-weight': 'bold', - 'font-size': '16px', - 'padding-top': '16px', - 'padding-bottom': '16px', - ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, - 'background-color': 'rgb(18, 151, 195)', - color: '#ffffff', - ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, - 'border-radius': '0px', - 'padding-left': '20px', - 'padding-right': '20px', + toggle: { + styles: { + toggle: { + 'font-family': 'Raleway, sans-serif', + 'font-weight': 'bold', + 'background-color': 'rgb(18, 151, 195)', + color: '#ffffff', + ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, + ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, + }, + count: { 'font-size': '16px' }, }, - quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, - title: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', 'font-size': '26px', color: '#4c4c4c' }, - price: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '18px', color: '#4c4c4c' }, - compareAt: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: '#4c4c4c' }, - unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: '#4c4c4c' }, - description: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '14px', color: '#4c4c4c' }, + googleFonts: ['Raleway'], }, - googleFonts: ['Raleway'], - text: { button: 'Add to cart' }, - }, - option: { - styles: { - label: { 'font-family': 'Raleway, sans-serif' }, - select: { 'font-family': 'Raleway, sans-serif' }, - }, - googleFonts: ['Raleway'], - }, - cart: { - styles: { - button: { - 'font-family': 'Raleway, sans-serif', - 'font-weight': 'bold', - 'font-size': '16px', - 'padding-top': '16px', - 'padding-bottom': '16px', - ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, - 'background-color': 'rgb(18, 151, 195)', - color: '#ffffff', - ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, - 'border-radius': '0px', - }, - }, - text: { - total: 'Subtotal', - notice: 'Shipping and discount codes are added at checkout - powered by Agora Markets', - button: 'Checkout', - noteDescription: 'Additional Information for the deflock.org team', - }, - googleFonts: ['Raleway'], - }, - toggle: { - styles: { - toggle: { - 'font-family': 'Raleway, sans-serif', - 'font-weight': 'bold', - 'background-color': 'rgb(18, 151, 195)', - color: '#ffffff', - ':hover': { 'background-color': '#0081ac', color: '#ffffff' }, - ':focus': { 'background-color': '#0081ac', color: '#ffffff' }, - }, - count: { 'font-size': '16px' }, - }, - googleFonts: ['Raleway'], - }, -}; + }; +} + +// ── Theme ─────────────────────────────────────────────────────────────────────── + +const theme = useTheme(); +const isDark = computed(() => theme.global.current.value.dark); // ── Tabs ──────────────────────────────────────────────────────────────────────── @@ -424,9 +462,9 @@ const collectionSelectItems = computed(() => { const collectionId = ref((route.query.category as string) || ALL_COLLECTION_ID); const shopifyContainer = ref(null); +const shopifyReady = ref(false); // Sync tab + category to URL so back/forward/refresh restores state. -// Guard prevents re-pushing when the ref update came FROM the router. watch([activeTab, collectionId], ([tab, category]) => { const currentTab = (route.query.tab as string) || 'shop'; const currentCategory = (route.query.category as string) || ALL_COLLECTION_ID; @@ -459,6 +497,7 @@ function initShopify(id: string) { const container = shopifyContainer.value; if (!container) return; + shopifyReady.value = false; container.innerHTML = ''; const client = window.ShopifyBuy.buildClient({ @@ -470,12 +509,20 @@ function initShopify(id: string) { id, node: container, moneyFormat: '%24%7B%7Bamount%7D%7D', - options: SHOPIFY_OPTIONS, + options: buildShopifyOptions(isDark.value), }); + // The SDK renders synchronously into the node before this callback resolves, + // so flipping the flag here reliably hides the skeleton at the right moment. + shopifyReady.value = true; }); - } +// Re-render the Shopify embed whenever the Vuetify theme flips so the modal +// text colours stay readable in both light and dark mode. +watch(isDark, () => { + if (window.ShopifyBuy?.UI) initShopify(collectionId.value); +}); + function loadShopifySDK() { if (window.ShopifyBuy?.UI) { initShopify(collectionId.value); @@ -552,9 +599,6 @@ const getTypeIcon = (type: string): string => const formatDate = (dateString: string) => new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); -// Watch the container ref so we initialise only once the DOM element exists. -// The `eager` prop on the window item makes it available immediately, but -// watching is more robust than relying on onMounted timing. watch(shopifyContainer, (el) => { if (el) loadShopifySDK(); }, { once: true }); From 2ea5b6cf06cc0aee5f08ab1959e1ad18fbcce0dc Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:05:22 -0400 Subject: [PATCH 14/24] Theme shop tab to match printables, add dark mode support to Shopify embed Theme shop tab to match printables, add dark mode support to Shopify embed Shop tab layout - Wrap Shopify embed in v-card (elevation 2) to match printables card style - Add skeleton loaders while the Buy Button SDK initializes - Add subtitle blurb above the category filter, consistent with printables tab Dark mode support - Add useTheme() / isDark computed to track the active Vuetify theme - Convert SHOPIFY_OPTIONS to buildShopifyOptions(dark) so all SDK-rendered surfaces can be re-themed on toggle - Theme product tile background + text (previously hardcoded white text with no background, invisible in light mode) - Theme product modal background (previously always rendered white regardless of site theme) - Theme cart/checkout drawer background, header, line items, and footer (previously always rendered light mode) - Re-run initShopify() via a watcher on isDark so the embed re-renders when the user toggles theme - Round button border-radius from 0px to 4px across product, modal, and cart buttons for visual consistency with printables cards Bug fixes - Add shopifyRenderToken generation guard in initShopify() to prevent a race condition where overlapping calls (e.g. theme settling shortly after mount) could leave the product grid partially rendered with missing images - Move subtitle blurb above the category filter dropdown (was below) Files changed: src/pages/StorePage.vue --- webapp/src/views/Store.vue | 58 +++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 4a3c4c0..9d45fa7 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -24,7 +24,11 @@ - +

+ T-shirts, stickers, yard signs, and more — proceeds support the anti-surveillance movement. +

+ + -

- T-shirts, stickers, yard signs, and more — proceeds support the anti-surveillance movement. -

- > = { }, }; -// Modal text uses a light-on-dark or dark-on-light palette depending on the -// active Vuetify theme. The product grid tiles always render on the SDK's own -// dark card surface so their text stays white regardless of theme. +// Every surface the Shopify SDK renders (product tiles, the product modal, +// and the cart drawer) gets an explicit background + text color pulled from +// this palette, since the SDK has no awareness of the host page's theme and +// otherwise falls back to its own static (light) defaults. function buildShopifyOptions(dark: boolean) { - const modalText = dark ? '#e0e0e0' : '#4c4c4c'; - const modalText2 = dark ? '#b0b0b0' : '#6b6b6b'; + const surfaceBg = dark ? '#1e1e1e' : '#ffffff'; + const surfaceBg2 = dark ? '#2a2a2a' : '#f5f5f5'; // footer/header accent surface + const modalText = dark ? '#e0e0e0' : '#2c2c2c'; + const modalText2 = dark ? '#b0b0b0' : '#6b6b6b'; + const borderColor = dark ? '#3a3a3a' : '#e0e0e0'; return { product: { styles: { product: { + 'background-color': surfaceBg, '@media (min-width: 601px)': { 'max-width': 'calc(33% - 20px)', 'margin-left': '20px', @@ -316,7 +321,7 @@ function buildShopifyOptions(dark: boolean) { width: 'calc(33% - 20px)', }, }, - title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, + title: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText }, button: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', @@ -332,10 +337,10 @@ function buildShopifyOptions(dark: boolean) { 'padding-right': '20px', }, quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, - price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: '#ffffff' }, - compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, - unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: '#ffffff' }, - description: { 'font-family': 'Raleway, sans-serif' }, + price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText }, + compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 }, + unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 }, + description: { 'font-family': 'Raleway, sans-serif', color: modalText2 }, }, buttonDestination: 'modal', contents: { button: false, options: false }, @@ -354,7 +359,11 @@ function buildShopifyOptions(dark: boolean) { modalProduct: { contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true }, styles: { - product: { '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' } }, + modal: { 'background-color': surfaceBg }, + product: { + 'background-color': surfaceBg, + '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' }, + }, button: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', @@ -388,6 +397,13 @@ function buildShopifyOptions(dark: boolean) { }, cart: { styles: { + cart: { 'background-color': surfaceBg }, + header: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor }, + lineItems: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor }, + footer: { 'background-color': surfaceBg2, color: modalText, 'border-color': borderColor }, + title: { color: modalText }, + notice: { color: modalText2 }, + currency: { color: modalText }, button: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', @@ -493,10 +509,18 @@ declare global { interface Window { ShopifyBuy: any } } +// Guards against overlapping initShopify calls. If the collection or theme +// changes again before a previous onReady() promise resolves, the stale +// callback below becomes a no-op instead of racing the newer one and +// leaving the container half-rendered (some product images missing). +let shopifyRenderToken = 0; + function initShopify(id: string) { const container = shopifyContainer.value; if (!container) return; + const myToken = ++shopifyRenderToken; + shopifyReady.value = false; container.innerHTML = ''; @@ -505,6 +529,8 @@ function initShopify(id: string) { storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', }); window.ShopifyBuy.UI.onReady(client).then((ui: any) => { + if (myToken !== shopifyRenderToken) return; // a newer render superseded this one + ui.createComponent('collection', { id, node: container, From ab571bc67be67c42511c81d65f8b028f3e3f393a Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:14:51 -0400 Subject: [PATCH 15/24] Fix Shopify embed theming: modal/cart stuck on stale theme, unreadable quantity stepper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the cart drawer and product modal are SDK singletons attached outside shopifyContainer, not children of it. Destroying and recreating the collection component on theme change only rebuilt the product grid — the cart/modal never got torn down, so they stayed frozen on whatever theme was active the first time they rendered. - Replace destroy-and-recreate with ui.createComponent() + updateConfig() on a persisted component reference, so theme changes push new options into the cart/modal singletons in place instead of leaving them stale - Rename initShopify() -> renderShopify() to reflect create-or-update behavior - Drop the shopifyRenderToken race guard from the previous commit — no longer needed since we're not destroying/rebuilding DOM on each change - Theme quantityInput (background, text, border) in both the product tile and modal; it was left on the SDK's static default and stayed unreadable regardless of theme Files changed: src/pages/StorePage.vue --- webapp/src/views/Store.vue | 70 +++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 9d45fa7..7fc488b 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -336,7 +336,14 @@ function buildShopifyOptions(dark: boolean) { 'padding-left': '20px', 'padding-right': '20px', }, - quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, + quantityInput: { + 'font-size': '16px', + 'padding-top': '16px', + 'padding-bottom': '16px', + 'background-color': surfaceBg2, + color: modalText, + 'border-color': borderColor, + }, price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText }, compareAt: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 }, unitPrice: { 'font-family': 'Raleway, sans-serif', 'font-size': '14.45px', color: modalText2 }, @@ -378,7 +385,14 @@ function buildShopifyOptions(dark: boolean) { 'padding-left': '20px', 'padding-right': '20px', }, - quantityInput: { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px' }, + quantityInput: { + 'font-size': '16px', + 'padding-top': '16px', + 'padding-bottom': '16px', + 'background-color': surfaceBg2, + color: modalText, + 'border-color': borderColor, + }, title: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', 'font-size': '26px', color: modalText }, price: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '18px', color: modalText }, compareAt: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'normal', 'font-size': '15.3px', color: modalText2 }, @@ -502,62 +516,70 @@ watch(() => route.query, (query) => { }); watch(collectionId, (id) => { - if (window.ShopifyBuy?.UI) initShopify(id); + if (window.ShopifyBuy?.UI) renderShopify(id); }); declare global { interface Window { ShopifyBuy: any } } -// Guards against overlapping initShopify calls. If the collection or theme -// changes again before a previous onReady() promise resolves, the stale -// callback below becomes a no-op instead of racing the newer one and -// leaving the container half-rendered (some product images missing). -let shopifyRenderToken = 0; +// The cart drawer and product modal are singletons the SDK attaches outside +// `shopifyContainer` (not as children of it), tied to the client for the +// lifetime of the page. Destroying and recreating the collection component +// on every theme change only rebuilds the product grid — the cart/modal +// singletons are untouched and stay stuck on whatever theme was active the +// first time they were created. Keeping a reference and calling +// updateConfig() instead pushes new styles into all of them in place. +let shopifyComponent: any = null; -function initShopify(id: string) { +function renderShopify(id: string) { const container = shopifyContainer.value; - if (!container) return; + if (!container || !window.ShopifyBuy?.UI) return; - const myToken = ++shopifyRenderToken; + const options = buildShopifyOptions(isDark.value); + + if (shopifyComponent?.updateConfig) { + shopifyComponent.updateConfig({ + id, + node: container, + moneyFormat: '%24%7B%7Bamount%7D%7D', + options, + }); + shopifyReady.value = true; + return; + } shopifyReady.value = false; - container.innerHTML = ''; - const client = window.ShopifyBuy.buildClient({ domain: 'agora.markets', storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', }); window.ShopifyBuy.UI.onReady(client).then((ui: any) => { - if (myToken !== shopifyRenderToken) return; // a newer render superseded this one - - ui.createComponent('collection', { + shopifyComponent = ui.createComponent('collection', { id, node: container, moneyFormat: '%24%7B%7Bamount%7D%7D', - options: buildShopifyOptions(isDark.value), + options, }); - // The SDK renders synchronously into the node before this callback resolves, - // so flipping the flag here reliably hides the skeleton at the right moment. shopifyReady.value = true; }); } -// Re-render the Shopify embed whenever the Vuetify theme flips so the modal -// text colours stay readable in both light and dark mode. +// Push new styles into the existing component whenever the Vuetify theme +// flips, instead of tearing down and rebuilding the whole widget. watch(isDark, () => { - if (window.ShopifyBuy?.UI) initShopify(collectionId.value); + renderShopify(collectionId.value); }); function loadShopifySDK() { if (window.ShopifyBuy?.UI) { - initShopify(collectionId.value); + renderShopify(collectionId.value); return; } const script = document.createElement('script'); script.async = true; script.src = 'https://sdks.shopifycdn.com/buy-button/latest/buy-button-storefront.min.js'; - script.onload = () => initShopify(collectionId.value); + script.onload = () => renderShopify(collectionId.value); document.head.appendChild(script); } From 2b5e1643b517e2e784d48309df5b1a7460ca2b58 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:30:23 -0400 Subject: [PATCH 16/24] Fix cart/collection duplication and reduce empty-cart risk in Shopify embed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes: - updateConfig() on collection switches did not clear previously-rendered product tiles before adding the new collection's, so switching categories stacked the new grid below the old one instead of replacing it - Theme toggles could fire overlapping async onReady() callbacks (if isDark settles in more than one tick), and the second createComponent() call landed on top of the first instead of replacing it, producing duplicate tiles - buildClient() was being called fresh on every theme/collection change instead of once per page load, increasing exposure to the pre-existing "empty cart on page load" issue by triggering it on every toggle Fixes: - Add getShopifyUI() to bootstrap buildClient()/UI.onReady() exactly once and cache the promise; every render reuses the same client/UI going forward, matching original page-load behavior - Remove the updateConfig() shortcut entirely — collection switches and theme changes now go through the same single render path - Always destroyShopifyComponents() (collection plus any auto-spawned cart/toggle) and clear the container before rebuilding, so no stale DOM can persist across a switch - Reintroduce a shopifyRenderToken guard so a stale async callback from a superseded render becomes a no-op instead of creating duplicate tiles Files changed: src/pages/StorePage.vue --- webapp/src/views/Store.vue | 89 +++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 7fc488b..8bf8a6c 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -523,52 +523,80 @@ declare global { interface Window { ShopifyBuy: any } } -// The cart drawer and product modal are singletons the SDK attaches outside -// `shopifyContainer` (not as children of it), tied to the client for the -// lifetime of the page. Destroying and recreating the collection component -// on every theme change only rebuilds the product grid — the cart/modal -// singletons are untouched and stay stuck on whatever theme was active the -// first time they were created. Keeping a reference and calling -// updateConfig() instead pushes new styles into all of them in place. +// The client/checkout is built exactly once and reused for the lifetime of +// the page. Rebuilding it on every theme or collection change risked +// re-triggering whatever caused the earlier "empty cart on page load" bug — +// now on every toggle instead of just once per load. +let shopifyUIPromise: Promise | null = null; +let shopifyUI: any = null; let shopifyComponent: any = null; +let shopifyRenderToken = 0; -function renderShopify(id: string) { - const container = shopifyContainer.value; - if (!container || !window.ShopifyBuy?.UI) return; - - const options = buildShopifyOptions(isDark.value); - - if (shopifyComponent?.updateConfig) { - shopifyComponent.updateConfig({ - id, - node: container, - moneyFormat: '%24%7B%7Bamount%7D%7D', - options, - }); - shopifyReady.value = true; - return; - } - - shopifyReady.value = false; +function getShopifyUI(): Promise { + if (shopifyUIPromise) return shopifyUIPromise; const client = window.ShopifyBuy.buildClient({ domain: 'agora.markets', storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', }); - window.ShopifyBuy.UI.onReady(client).then((ui: any) => { + shopifyUIPromise = window.ShopifyBuy.UI.onReady(client).then((ui: any) => { + shopifyUI = ui; + return ui; + }); + return shopifyUIPromise; +} + +// The cart drawer and product modal are singletons the SDK attaches outside +// `shopifyContainer` (not as children of it). Destroys every component +// instance the SDK created (collection, plus whatever cart/toggle it +// auto-spawned) so a rebuild always starts from a clean slate — updateConfig() +// was tried here previously but doesn't reliably clear already-mounted DOM, +// which caused old collections/themes to linger alongside new ones. +function destroyShopifyComponents() { + if (!shopifyUI?.components) return; + Object.values(shopifyUI.components).forEach((instances: any) => { + (instances as any[]).forEach((component) => { + try { + component?.destroy?.(); + } catch { + // Component may already be detached; safe to ignore. + } + }); + }); + shopifyComponent = null; +} + +function renderShopify(id: string) { + const container = shopifyContainer.value; + if (!container) return; + + // Guards against overlapping renders (e.g. the theme value settling in + // two quick ticks). If a newer render starts before this one's async + // onReady() resolves, the stale callback below becomes a no-op instead + // of creating a second, duplicate set of product tiles. + const myToken = ++shopifyRenderToken; + shopifyReady.value = false; + + getShopifyUI().then((ui: any) => { + if (myToken !== shopifyRenderToken) return; + + destroyShopifyComponents(); + container.innerHTML = ''; + shopifyComponent = ui.createComponent('collection', { id, node: container, moneyFormat: '%24%7B%7Bamount%7D%7D', - options, + options: buildShopifyOptions(isDark.value), }); shopifyReady.value = true; }); } -// Push new styles into the existing component whenever the Vuetify theme -// flips, instead of tearing down and rebuilding the whole widget. +// Rebuild the widget whenever the Vuetify theme flips, so the cart drawer +// and product modal singletons pick up the new theme immediately instead +// of requiring a page refresh. watch(isDark, () => { - renderShopify(collectionId.value); + if (window.ShopifyBuy?.UI) renderShopify(collectionId.value); }); function loadShopifySDK() { @@ -583,6 +611,7 @@ function loadShopifySDK() { document.head.appendChild(script); } + // ── Printables ────────────────────────────────────────────────────────────────── interface Printable { From cb5a5313915fbd0ed4a62423b3d1d36346732488 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:44:30 -0400 Subject: [PATCH 17/24] Fix CLaude Hallucinations on how to re-create theming, MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept cart/modal theme-refresh limitation per BuyButton.js docs (Option A) Looked up the actual Shopify BuyButton.js source and docs instead of guessing at API surface. Confirmed there is no documented destroy() or updateConfig() method — createComponent(type, config) is the only supported way to create or change what's rendered, and it auto-creates a separate cart + "cart toggle" component tied to that call, not a child of the node you pass in. - Revert to plain clear-and-recreate (container.innerHTML + createComponent) for both collection switches and theme toggles — the only pattern the SDK actually documents - Remove all destroy()/updateConfig()/ui.components calls added in prior revisions; they relied on unverified internal APIs and were producing empty renders more often than not - Keep the cached client (getShopifyUI(), built once) to avoid re-risking the "empty cart on page load" issue on every toggle - Keep the shopifyRenderToken guard against overlapping async renders - Document the accepted limitation in code: product grid re-themes live on every toggle; cart drawer and product modal pick up a new theme on next page load only, since there's no documented way to re-theme those singletons without a full reload Files changed: src/pages/StorePage.vue --- webapp/src/views/Store.vue | 53 +++++++++++++------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 8bf8a6c..72df961 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -524,12 +524,9 @@ declare global { } // The client/checkout is built exactly once and reused for the lifetime of -// the page. Rebuilding it on every theme or collection change risked -// re-triggering whatever caused the earlier "empty cart on page load" bug — -// now on every toggle instead of just once per load. +// the page, so re-rendering (theme or collection change) doesn't risk +// re-triggering the "empty cart on page load" issue on every toggle. let shopifyUIPromise: Promise | null = null; -let shopifyUI: any = null; -let shopifyComponent: any = null; let shopifyRenderToken = 0; function getShopifyUI(): Promise { @@ -538,33 +535,23 @@ function getShopifyUI(): Promise { domain: 'agora.markets', storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', }); - shopifyUIPromise = window.ShopifyBuy.UI.onReady(client).then((ui: any) => { - shopifyUI = ui; - return ui; - }); + shopifyUIPromise = window.ShopifyBuy.UI.onReady(client); return shopifyUIPromise; } -// The cart drawer and product modal are singletons the SDK attaches outside -// `shopifyContainer` (not as children of it). Destroys every component -// instance the SDK created (collection, plus whatever cart/toggle it -// auto-spawned) so a rebuild always starts from a clean slate — updateConfig() -// was tried here previously but doesn't reliably clear already-mounted DOM, -// which caused old collections/themes to linger alongside new ones. -function destroyShopifyComponents() { - if (!shopifyUI?.components) return; - Object.values(shopifyUI.components).forEach((instances: any) => { - (instances as any[]).forEach((component) => { - try { - component?.destroy?.(); - } catch { - // Component may already be detached; safe to ignore. - } - }); - }); - shopifyComponent = null; -} - +// Plain clear-and-recreate, matching the only pattern BuyButton.js +// documents: ui.createComponent(type, config) with a node to render into. +// There is no documented destroy()/updateConfig() API — a prior revision +// guessed at both and produced empty renders, since createComponent() with +// a previously-used node confused the SDK's internal state. +// +// Per Shopify's own docs, creating a product/collection component also +// auto-creates a separate cart and "cart toggle" component tied to that +// call — they are not children of `container` and there is no documented +// way to re-theme them without a full page reload. That's an accepted, +// known limitation (Option A): the product grid re-themes live on every +// toggle; the cart drawer and product modal pick up the new theme on the +// next page load. function renderShopify(id: string) { const container = shopifyContainer.value; if (!container) return; @@ -579,10 +566,8 @@ function renderShopify(id: string) { getShopifyUI().then((ui: any) => { if (myToken !== shopifyRenderToken) return; - destroyShopifyComponents(); container.innerHTML = ''; - - shopifyComponent = ui.createComponent('collection', { + ui.createComponent('collection', { id, node: container, moneyFormat: '%24%7B%7Bamount%7D%7D', @@ -592,9 +577,6 @@ function renderShopify(id: string) { }); } -// Rebuild the widget whenever the Vuetify theme flips, so the cart drawer -// and product modal singletons pick up the new theme immediately instead -// of requiring a page refresh. watch(isDark, () => { if (window.ShopifyBuy?.UI) renderShopify(collectionId.value); }); @@ -612,6 +594,7 @@ function loadShopifySDK() { } + // ── Printables ────────────────────────────────────────────────────────────────── interface Printable { From 8b9ed178a53ee5bd6766b43660544b14a68c9f50 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:48:43 -0400 Subject: [PATCH 18/24] Update Store.vue --- webapp/src/views/Store.vue | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 72df961..02d516e 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -523,28 +523,22 @@ declare global { interface Window { ShopifyBuy: any } } -// The client/checkout is built exactly once and reused for the lifetime of -// the page, so re-rendering (theme or collection change) doesn't risk -// re-triggering the "empty cart on page load" issue on every toggle. -let shopifyUIPromise: Promise | null = null; let shopifyRenderToken = 0; -function getShopifyUI(): Promise { - if (shopifyUIPromise) return shopifyUIPromise; - const client = window.ShopifyBuy.buildClient({ - domain: 'agora.markets', - storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', - }); - shopifyUIPromise = window.ShopifyBuy.UI.onReady(client); - return shopifyUIPromise; -} - // Plain clear-and-recreate, matching the only pattern BuyButton.js // documents: ui.createComponent(type, config) with a node to render into. // There is no documented destroy()/updateConfig() API — a prior revision // guessed at both and produced empty renders, since createComponent() with // a previously-used node confused the SDK's internal state. // +// A fresh client + ui is built on every render rather than cached and +// reused. Caching was tried to reduce how often buildClient() runs (out of +// concern for the earlier "empty cart on page load" bug), but reusing the +// same ui instance across multiple createComponent() calls broke the +// product grid on theme change — same category of problem as the +// destroy()/updateConfig() guesswork, just from a different angle. This is +// the version that was confirmed working for live grid theme-switching. +// // Per Shopify's own docs, creating a product/collection component also // auto-creates a separate cart and "cart toggle" component tied to that // call — they are not children of `container` and there is no documented @@ -554,7 +548,7 @@ function getShopifyUI(): Promise { // next page load. function renderShopify(id: string) { const container = shopifyContainer.value; - if (!container) return; + if (!container || !window.ShopifyBuy?.UI) return; // Guards against overlapping renders (e.g. the theme value settling in // two quick ticks). If a newer render starts before this one's async @@ -563,7 +557,11 @@ function renderShopify(id: string) { const myToken = ++shopifyRenderToken; shopifyReady.value = false; - getShopifyUI().then((ui: any) => { + const client = window.ShopifyBuy.buildClient({ + domain: 'agora.markets', + storefrontAccessToken: '78991208f7fea14aa4ac02a58f8025dd', + }); + window.ShopifyBuy.UI.onReady(client).then((ui: any) => { if (myToken !== shopifyRenderToken) return; container.innerHTML = ''; From 2ac998cf4c5b7dbd2501e6370bf9fb66fb332972 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:54:57 -0400 Subject: [PATCH 19/24] Update Store.vue --- webapp/src/views/Store.vue | 55 +++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index 02d516e..a565e88 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -340,8 +340,8 @@ function buildShopifyOptions(dark: boolean) { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px', - 'background-color': surfaceBg2, - color: modalText, + 'background-color': `${surfaceBg2} !important`, + color: `${modalText} !important`, 'border-color': borderColor, }, price: { 'font-family': 'Raleway, sans-serif', 'font-size': '17px', color: modalText }, @@ -366,7 +366,6 @@ function buildShopifyOptions(dark: boolean) { modalProduct: { contents: { img: false, imgWithCarousel: true, button: false, buttonWithQuantity: true }, styles: { - modal: { 'background-color': surfaceBg }, product: { 'background-color': surfaceBg, '@media (min-width: 601px)': { 'max-width': '100%', 'margin-left': '0px', 'margin-bottom': '0px' }, @@ -389,8 +388,8 @@ function buildShopifyOptions(dark: boolean) { 'font-size': '16px', 'padding-top': '16px', 'padding-bottom': '16px', - 'background-color': surfaceBg2, - color: modalText, + 'background-color': `${surfaceBg2} !important`, + color: `${modalText} !important`, 'border-color': borderColor, }, title: { 'font-family': 'Raleway, sans-serif', 'font-weight': 'bold', 'font-size': '26px', color: modalText }, @@ -413,7 +412,6 @@ function buildShopifyOptions(dark: boolean) { styles: { cart: { 'background-color': surfaceBg }, header: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor }, - lineItems: { 'background-color': surfaceBg, color: modalText, 'border-color': borderColor }, footer: { 'background-color': surfaceBg2, color: modalText, 'border-color': borderColor }, title: { color: modalText }, notice: { color: modalText2 }, @@ -453,6 +451,51 @@ function buildShopifyOptions(dark: boolean) { }, googleFonts: ['Raleway'], }, + // `lineItem` is its own top-level component (a sibling of `cart`, not + // nested inside it) — confirmed against real working configs in + // Shopify's community forum. A previous revision nested this under + // cart.styles.lineItems, which isn't a real key and silently did + // nothing, leaving each cart row's quantity stepper on the SDK's + // unstyled default (white input, illegible in dark mode). + lineItem: { + styles: { + title: { color: modalText }, + itemTitle: { color: modalText }, // some SDK builds use this key instead of `title` + variantTitle: { color: modalText2 }, + price: { color: modalText }, + fullPrice: { color: modalText }, + discount: { color: modalText2 }, + discountIcon: { fill: modalText2 }, + quantity: { color: modalText }, + quantityIncrement: { + color: `${modalText} !important`, + 'border-color': borderColor, + 'background-color': `${surfaceBg2} !important`, + }, + quantityDecrement: { + color: `${modalText} !important`, + 'border-color': borderColor, + 'background-color': `${surfaceBg2} !important`, + }, + quantityInput: { + color: `${modalText} !important`, + 'border-color': borderColor, + 'background-color': `${surfaceBg2} !important`, + }, + }, + }, + // `modal` is the pop-up wrapper itself and is a separate top-level + // component from `modalProduct` (which styles the product content + // inside it) — a previous revision nested this inside modalProduct, + // where it likely had no effect. This key name is a best-effort guess + // following the self-named pattern seen in `cart.styles.cart` and + // `toggle.styles.toggle`; it hasn't been confirmed against a working + // example the way the lineItem keys above have. + modal: { + styles: { + modal: { 'background-color': surfaceBg }, + }, + }, }; } From 160ed01e0656413b3010f0f77923d0ece54d0898 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:00:28 -0400 Subject: [PATCH 20/24] Update Store.vue --- webapp/src/views/Store.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index a565e88..d2f40cf 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -47,6 +47,9 @@ + From 5f249567847c37d72e88ca681525337117709668 Mon Sep 17 00:00:00 2001 From: Daniel McGee <134719124+mcgeeandme@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:29:05 -0400 Subject: [PATCH 21/24] Update Store.vue --- webapp/src/views/Store.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/webapp/src/views/Store.vue b/webapp/src/views/Store.vue index d2f40cf..624b03b 100644 --- a/webapp/src/views/Store.vue +++ b/webapp/src/views/Store.vue @@ -24,12 +24,12 @@ -

+

T-shirts, stickers, yard signs, and more — proceeds support the anti-surveillance movement.

- +