create-shopify-firebase-app 2.0.6 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Products page logic
3
3
  * Search, display product cards, show detail modal, resource picker demo.
4
+ * Uses Polaris Web Components for all UI rendering.
4
5
  */
5
6
 
6
7
  (function () {
@@ -9,17 +10,27 @@
9
10
  var searchInput = null;
10
11
  var container = null;
11
12
  var resultsCount = null;
13
+ var resultsSection = null;
12
14
 
13
15
  // ── Init ────────────────────────────────────────────────────────
16
+ // Show the count card only when there is a count. An empty <s-section>
17
+ // still renders as a padded card, which reads as a stray blank box.
18
+ function setResultsCount(text) {
19
+ if (resultsCount) resultsCount.textContent = text || "";
20
+ if (resultsSection) resultsSection.style.display = text ? "" : "none";
21
+ }
22
+
14
23
  document.addEventListener("DOMContentLoaded", function () {
15
24
  searchInput = document.getElementById("search-input");
16
25
  container = document.getElementById("products-container");
17
26
  resultsCount = document.getElementById("results-count");
27
+ resultsSection = document.getElementById("results-section");
18
28
 
19
29
  if (searchInput) {
20
- searchInput.addEventListener("input", debounce(handleSearch, 400));
30
+ var debouncedSearch = debounce(handleSearch, 400);
31
+ searchInput.addEventListener("input", debouncedSearch);
32
+ searchInput.addEventListener("change", debouncedSearch);
21
33
 
22
- // Search on Enter key
23
34
  searchInput.addEventListener("keydown", function (e) {
24
35
  if (e.key === "Enter") {
25
36
  e.preventDefault();
@@ -35,49 +46,51 @@
35
46
 
36
47
  if (!query) {
37
48
  container.innerHTML =
38
- '<div class="card">' +
39
- '<div class="empty-state">' +
40
- '<div class="empty-state-icon">&#128722;</div>' +
41
- "<h3>Search for products</h3>" +
42
- "<p>Enter a search term above to find products in your store, or use the Resource Picker to browse.</p>" +
43
- '<button class="btn btn-primary" onclick="document.getElementById(\'search-input\').focus()">Start searching</button>' +
44
- "</div></div>";
45
- if (resultsCount) resultsCount.textContent = "";
49
+ '<s-box padding="large-1200" border="base" borderRadius="base">' +
50
+ '<s-stack alignItems="center" gap="base">' +
51
+ '<s-text variant="headingMd">Search for products</s-text>' +
52
+ '<s-text color="subdued">Enter a search term above to find products in your store, or use the Resource Picker to browse.</s-text>' +
53
+ '<s-button variant="primary" onclick="document.getElementById(\'search-input\').focus()">Start searching</s-button>' +
54
+ "</s-stack></s-box>";
55
+ setResultsCount("");
46
56
  return;
47
57
  }
48
58
 
49
- // Show loading skeleton
50
- container.innerHTML = renderSkeletonGrid();
59
+ // Show loading
60
+ container.innerHTML =
61
+ '<s-box padding="large-400">' +
62
+ '<s-stack alignItems="center" gap="base">' +
63
+ "<s-spinner></s-spinner>" +
64
+ '<s-text color="subdued">Searching...</s-text>' +
65
+ "</s-stack></s-box>";
51
66
 
52
67
  try {
53
68
  var data = await apiFetch("/api/products/search?q=" + encodeURIComponent(query));
54
69
  var products = data.products || [];
55
70
 
56
- if (resultsCount) {
57
- resultsCount.textContent = products.length + " product" + (products.length !== 1 ? "s" : "") + " found";
71
+ {
72
+ setResultsCount(products.length + " product" + (products.length !== 1 ? "s" : "") + " found");
58
73
  }
59
74
 
60
75
  if (products.length === 0) {
61
76
  container.innerHTML =
62
- '<div class="card">' +
63
- '<div class="empty-state">' +
64
- '<div class="empty-state-icon">&#128270;</div>' +
65
- "<h3>No products found</h3>" +
66
- '<p>No products match "' + escapeHtml(query) + '". Try a different search term.</p>' +
67
- "</div></div>";
77
+ '<s-box padding="large-1200" border="base" borderRadius="base">' +
78
+ '<s-stack alignItems="center" gap="base">' +
79
+ '<s-text variant="headingMd">No products found</s-text>' +
80
+ '<s-text color="subdued">No products match "' + escapeHtml(query) + '". Try a different search term.</s-text>' +
81
+ "</s-stack></s-box>";
68
82
  return;
69
83
  }
70
84
 
71
85
  renderProductGrid(products);
72
86
  } catch (err) {
73
87
  container.innerHTML =
74
- '<div class="banner banner-critical">' +
75
- '<div class="banner-icon">&#9888;</div>' +
76
- '<div class="banner-content">' +
77
- "<p><strong>Search failed</strong></p>" +
78
- "<p>" + escapeHtml(err.message) + "</p>" +
79
- "</div></div>";
80
- if (resultsCount) resultsCount.textContent = "";
88
+ '<s-banner tone="critical">' +
89
+ "<s-text>" +
90
+ "<s-text fontWeight=\"semibold\">Search failed</s-text> " +
91
+ escapeHtml(err.message) +
92
+ "</s-text></s-banner>";
93
+ setResultsCount("");
81
94
  }
82
95
  }
83
96
 
@@ -98,48 +111,31 @@
98
111
  '<div class="product-card-image">' +
99
112
  (image
100
113
  ? '<img src="' + escapeAttr(image) + '" alt="' + escapeAttr(p.title || "") + '" loading="lazy">'
101
- : '<span>No image</span>') +
114
+ : '<s-text color="subdued">No image</s-text>') +
102
115
  "</div>" +
103
116
  '<div class="product-card-body">' +
104
- '<div class="product-card-title">' + escapeHtml(p.title || "Untitled") + "</div>" +
105
- (vendor ? '<div class="product-card-vendor">' + escapeHtml(vendor) + "</div>" : "") +
106
- '<div class="product-card-meta">' +
107
- '<span class="product-card-price">' + escapeHtml(price) + "</span>" +
108
- '<span class="badge ' + status.badgeClass + '">' + escapeHtml(status.label) + "</span>" +
109
- "</div></div></div>";
117
+ '<s-text fontWeight="semibold">' + escapeHtml(p.title || "Untitled") + "</s-text>" +
118
+ (vendor ? '<s-text color="subdued" variant="bodySm">' + escapeHtml(vendor) + "</s-text>" : "") +
119
+ '<s-stack direction="inline" gap="small-200" alignItems="center" style="margin-top: 8px;">' +
120
+ '<s-text fontWeight="semibold">' + escapeHtml(price) + "</s-text>" +
121
+ '<s-badge tone="' + status.tone + '">' + escapeHtml(status.label) + "</s-badge>" +
122
+ "</s-stack></div></div>";
110
123
  }
111
124
 
112
125
  html += "</div>";
113
126
  container.innerHTML = html;
114
127
  }
115
128
 
116
- // ── Skeleton loading ────────────────────────────────────────────
117
- function renderSkeletonGrid() {
118
- var html = '<div class="product-grid">';
119
- for (var i = 0; i < 6; i++) {
120
- html +=
121
- '<div class="product-card">' +
122
- '<div class="skeleton skeleton-image"></div>' +
123
- '<div class="product-card-body">' +
124
- '<div class="skeleton skeleton-heading" style="width:80%;"></div>' +
125
- '<div class="skeleton skeleton-text" style="width:50%;"></div>' +
126
- '<div class="skeleton skeleton-text" style="width:60%;"></div>' +
127
- "</div></div>";
128
- }
129
- html += "</div>";
130
- return html;
131
- }
132
-
133
129
  // ── Show product detail in modal ────────────────────────────────
134
130
  window.showProductDetail = async function (productId) {
135
131
  var modal = document.getElementById("product-detail-modal");
136
132
  var content = document.getElementById("product-detail-content");
137
133
 
138
134
  content.innerHTML =
139
- '<div class="loading-state">' +
140
- '<div class="spinner"></div>' +
141
- "<p>Loading product details...</p>" +
142
- "</div>";
135
+ '<s-stack alignItems="center" gap="base">' +
136
+ "<s-spinner></s-spinner>" +
137
+ '<s-text color="subdued">Loading product details...</s-text>' +
138
+ "</s-stack>";
143
139
 
144
140
  modal.show();
145
141
 
@@ -149,19 +145,17 @@
149
145
  renderProductDetail(content, p);
150
146
  } catch (err) {
151
147
  content.innerHTML =
152
- '<div class="banner banner-critical">' +
153
- '<div class="banner-icon">&#9888;</div>' +
154
- '<div class="banner-content">' +
155
- "<p><strong>Could not load product</strong></p>" +
156
- "<p>" + escapeHtml(err.message) + "</p>" +
157
- "</div></div>";
148
+ '<s-banner tone="critical">' +
149
+ "<s-text>" +
150
+ "<s-text fontWeight=\"semibold\">Could not load product</s-text> " +
151
+ escapeHtml(err.message) +
152
+ "</s-text></s-banner>";
158
153
  }
159
154
  };
160
155
 
161
156
  function renderProductDetail(container, p) {
162
157
  var image = getProductImage(p);
163
158
  var status = getProductStatus(p);
164
- // GraphQL returns variants as { edges: [{ node: {...} }] }
165
159
  var variants = [];
166
160
  if (p.variants && p.variants.edges) {
167
161
  for (var vi = 0; vi < p.variants.edges.length; vi++) {
@@ -171,44 +165,40 @@
171
165
  variants = p.variants;
172
166
  }
173
167
 
174
- var html =
175
- '<div style="display:grid; grid-template-columns: 200px 1fr; gap: 20px; align-items: start;">';
168
+ var html = '<s-stack gap="base">';
169
+
170
+ // Image + info grid
171
+ html += '<s-grid gridTemplateColumns="@container (inline-size <= 400px) 1fr, 200px 1fr" gap="base" alignItems="start">';
176
172
 
177
173
  // Image
178
- html += '<div>';
179
174
  if (image) {
180
- html += '<img src="' + escapeAttr(image) + '" alt="' + escapeAttr(p.title || "") + '" style="width:100%; border-radius: 8px; border: 1px solid var(--p-color-border);">';
175
+ html += '<img src="' + escapeAttr(image) + '" alt="' + escapeAttr(p.title || "") + '" style="width:100%; border-radius: 8px; border: 1px solid #e1e3e5;">';
181
176
  } else {
182
- html += '<div style="width:100%; aspect-ratio:1; background: var(--p-color-bg); border-radius: 8px; display:flex; align-items:center; justify-content:center; color: var(--p-color-text-disabled); border: 1px solid var(--p-color-border);">No image</div>';
177
+ html += '<s-box padding="large-400" border="base" borderRadius="base" background="bg-surface-secondary"><s-stack alignItems="center"><s-text color="subdued">No image</s-text></s-stack></s-box>';
183
178
  }
184
- html += "</div>";
185
179
 
186
180
  // Info
187
- html += "<div>";
188
- html += '<h3 style="font-size: 18px; margin-bottom: 12px;">' + escapeHtml(p.title || "Untitled") + "</h3>";
189
-
190
- html += '<ul class="info-list">';
191
- html += infoRow("Status", '<span class="badge ' + status.badgeClass + '">' + escapeHtml(status.label) + "</span>");
192
- if (p.vendor) html += infoRow("Vendor", escapeHtml(p.vendor));
193
- // GraphQL uses productType (camelCase)
194
- if (p.productType) html += infoRow("Type", escapeHtml(p.productType));
195
- html += infoRow("Price", escapeHtml(getProductPrice(p)));
196
- if (p.totalInventory !== undefined) html += infoRow("Total inventory", String(p.totalInventory));
197
- html += "</ul>";
198
- html += "</div></div>";
199
-
200
- // Description — GraphQL uses "description" not "body_html"
181
+ html += '<s-stack gap="small-200">';
182
+ html += '<s-text variant="headingLg">' + escapeHtml(p.title || "Untitled") + "</s-text>";
183
+ html += '<s-stack gap="small-200">';
184
+ html += infoRow("Status", '<s-badge tone="' + status.tone + '">' + escapeHtml(status.label) + "</s-badge>");
185
+ if (p.vendor) html += infoRow("Vendor", "<s-text>" + escapeHtml(p.vendor) + "</s-text>");
186
+ if (p.productType) html += infoRow("Type", "<s-text>" + escapeHtml(p.productType) + "</s-text>");
187
+ html += infoRow("Price", "<s-text>" + escapeHtml(getProductPrice(p)) + "</s-text>");
188
+ if (p.totalInventory !== undefined) html += infoRow("Total inventory", "<s-text>" + escapeHtml(String(p.totalInventory)) + "</s-text>");
189
+ html += "</s-stack></s-stack>";
190
+ html += "</s-grid>";
191
+
192
+ // Description
201
193
  var desc = p.description || p.body_html || "";
202
194
  if (desc) {
203
- html += "<hr class='divider'>";
204
- html += '<h3 class="mb-2">Description</h3>';
205
- html += '<div class="text-secondary">' + escapeHtml(desc) + "</div>";
195
+ html += '<s-text variant="headingSm">Description</s-text>';
196
+ html += '<s-text color="subdued">' + escapeHtml(desc) + "</s-text>";
206
197
  }
207
198
 
208
199
  // Variants table
209
200
  if (variants.length > 0) {
210
- html += "<hr class='divider'>";
211
- html += '<h3 class="mb-3">Variants (' + variants.length + ")</h3>";
201
+ html += '<s-text variant="headingSm">Variants (' + variants.length + ")</s-text>";
212
202
  html += '<div class="table-container"><table>';
213
203
  html += "<thead><tr><th>Title</th><th>Price</th><th>SKU</th><th>Inventory</th></tr></thead><tbody>";
214
204
  for (var i = 0; i < variants.length; i++) {
@@ -217,21 +207,21 @@
217
207
  html += "<td>" + escapeHtml(v.title || "--") + "</td>";
218
208
  html += "<td>" + escapeHtml(v.price ? formatCurrency(v.price) : "--") + "</td>";
219
209
  html += "<td><code>" + escapeHtml(v.sku || "--") + "</code></td>";
220
- // GraphQL uses inventoryQuantity (camelCase)
221
- html += "<td>" + (v.inventoryQuantity !== undefined ? v.inventoryQuantity : (v.inventory_quantity !== undefined ? v.inventory_quantity : "--")) + "</td>";
210
+ html += "<td>" + escapeHtml(String(v.inventoryQuantity !== undefined ? v.inventoryQuantity : (v.inventory_quantity !== undefined ? v.inventory_quantity : "--"))) + "</td>";
222
211
  html += "</tr>";
223
212
  }
224
213
  html += "</tbody></table></div>";
225
214
  }
226
215
 
216
+ html += "</s-stack>";
227
217
  container.innerHTML = html;
228
218
  }
229
219
 
230
220
  function infoRow(label, valueHtml) {
231
- return "<li>" +
232
- '<span class="info-list-label">' + escapeHtml(label) + "</span>" +
233
- '<span class="info-list-value">' + valueHtml + "</span>" +
234
- "</li>";
221
+ return '<s-grid gridTemplateColumns="120px 1fr" gap="small-200" alignItems="center">' +
222
+ '<s-text color="subdued">' + escapeHtml(label) + "</s-text>" +
223
+ valueHtml +
224
+ "</s-grid>";
235
225
  }
236
226
 
237
227
  // ── Resource Picker ─────────────────────────────────────────────
@@ -252,17 +242,17 @@
252
242
  var modal = document.getElementById("picker-result-modal");
253
243
  var content = document.getElementById("picker-result-content");
254
244
 
255
- var html = '<p class="text-secondary mb-3">You selected ' + selected.length + " product(s):</p>";
256
- html += '<ul class="info-list">';
245
+ var html = '<s-stack gap="base">';
246
+ html += '<s-text color="subdued">You selected ' + selected.length + " product(s):</s-text>";
257
247
  for (var i = 0; i < selected.length; i++) {
258
248
  var item = selected[i];
259
- html += "<li>";
260
- html += '<span class="font-semibold">' + escapeHtml(item.title || "Untitled") + "</span>";
261
- html += '<span class="badge badge-info">Selected</span>';
262
- html += "</li>";
249
+ html += '<s-stack direction="inline" gap="base" alignItems="center">';
250
+ html += '<s-text fontWeight="semibold">' + escapeHtml(item.title || "Untitled") + "</s-text>";
251
+ html += '<s-badge tone="info">Selected</s-badge>';
252
+ html += "</s-stack>";
263
253
  }
264
- html += "</ul>";
265
- html += '<p class="text-secondary text-sm mt-4">Resource Picker returns product data you can use in your app logic.</p>';
254
+ html += '<s-text color="subdued" variant="bodySm">Resource Picker returns product data you can use in your app logic.</s-text>';
255
+ html += "</s-stack>";
266
256
 
267
257
  content.innerHTML = html;
268
258
  modal.show();
@@ -282,6 +272,10 @@
282
272
  }
283
273
 
284
274
  function getProductPrice(product) {
275
+ if (product.variants && product.variants.edges && product.variants.edges.length > 0) {
276
+ var price = product.variants.edges[0].node.price;
277
+ if (price) return formatCurrency(price);
278
+ }
285
279
  if (product.variants && product.variants.length > 0) {
286
280
  var price = product.variants[0].price;
287
281
  if (price) return formatCurrency(price);
@@ -295,25 +289,17 @@
295
289
 
296
290
  function getProductStatus(product) {
297
291
  var status = (product.status || "").toLowerCase();
298
- if (status === "active") return { label: "Active", badgeClass: "badge-success" };
299
- if (status === "draft") return { label: "Draft", badgeClass: "badge-default" };
300
- if (status === "archived") return { label: "Archived", badgeClass: "badge-warning" };
301
- return { label: status || "Active", badgeClass: "badge-success" };
292
+ if (status === "active") return { label: "Active", tone: "success" };
293
+ if (status === "draft") return { label: "Draft", tone: "info" };
294
+ if (status === "archived") return { label: "Archived", tone: "warning" };
295
+ return { label: status || "Active", tone: "success" };
302
296
  }
303
297
 
304
298
  function extractId(gid) {
305
- // Extract numeric ID from GID like "gid://shopify/Product/123"
306
299
  if (!gid) return "";
307
300
  var parts = String(gid).split("/");
308
301
  return parts[parts.length - 1] || gid;
309
302
  }
310
303
 
311
- function escapeAttr(str) {
312
- return String(str)
313
- .replace(/&/g, "&amp;")
314
- .replace(/"/g, "&quot;")
315
- .replace(/'/g, "&#39;")
316
- .replace(/</g, "&lt;")
317
- .replace(/>/g, "&gt;");
318
- }
304
+ // escapeAttr is provided globally by app.js
319
305
  })();