@agent-cms/agentcms 0.0.0-stage → 0.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.
- package/LICENSE +143 -0
- package/README.md +1119 -2
- package/changelog.json +20 -0
- package/dist/admin/admin-files.json +48 -0
- package/dist/admin/assets/CodeEditor-DcFzGUt8.js +24 -0
- package/dist/admin/assets/flexweg-logo-CdtuqAFA.png +0 -0
- package/dist/admin/assets/index-CLZ1gQZl.js +5460 -0
- package/dist/admin/assets/index-DMcGogR2.css +1 -0
- package/dist/admin/assets/sql-wasm-C1U8OeUW.wasm +0 -0
- package/dist/admin/assets/sql-wasm-CYl3LXH_.js +2 -0
- package/dist/admin/assets/sql-wasm-V5jTDcrT.js +1 -0
- package/dist/admin/config.js +11 -0
- package/dist/admin/external.default.json +72 -0
- package/dist/admin/index.html +116 -0
- package/dist/admin/plugins/core-seo/bundle.js +55 -0
- package/dist/admin/plugins/core-seo/manifest.json +7 -0
- package/dist/admin/plugins/flexweg-archives/bundle.js +1414 -0
- package/dist/admin/plugins/flexweg-archives/manifest.json +7 -0
- package/dist/admin/plugins/flexweg-multilang/bundle.js +1187 -0
- package/dist/admin/plugins/flexweg-multilang/manifest.json +7 -0
- package/dist/admin/plugins/flexweg-rss/bundle.js +1354 -0
- package/dist/admin/plugins/flexweg-rss/manifest.json +7 -0
- package/dist/admin/plugins/flexweg-search/bundle.js +1243 -0
- package/dist/admin/plugins/flexweg-search/manifest.json +7 -0
- package/dist/admin/plugins/flexweg-sitemaps/bundle.js +1293 -0
- package/dist/admin/plugins/flexweg-sitemaps/manifest.json +7 -0
- package/dist/admin/runtime/cms-runtime.js +164 -0
- package/dist/admin/runtime/react-dom-client.js +13 -0
- package/dist/admin/runtime/react-dom.js +16 -0
- package/dist/admin/runtime/react-i18next.js +21 -0
- package/dist/admin/runtime/react-jsx-runtime.js +17 -0
- package/dist/admin/runtime/react.js +55 -0
- package/dist/admin/themes/corporate/bundle.js +14465 -0
- package/dist/admin/themes/corporate/manifest.json +7 -0
- package/dist/admin/themes/corporate/theme.css +2 -0
- package/dist/admin/themes/magazine/bundle.js +10229 -0
- package/dist/admin/themes/magazine/manifest.json +7 -0
- package/dist/admin/themes/magazine/theme.css +2 -0
- package/dist/admin/themes/marketplace-core/bundle.js +11213 -0
- package/dist/admin/themes/marketplace-core/manifest.json +7 -0
- package/dist/admin/themes/marketplace-core/theme.css +1613 -0
- package/dist/admin/themes/portfolio/bundle.js +9195 -0
- package/dist/admin/themes/portfolio/manifest.json +7 -0
- package/dist/admin/themes/portfolio/theme.css +2 -0
- package/dist/admin/themes/storefront/bundle.js +13172 -0
- package/dist/admin/themes/storefront/manifest.json +7 -0
- package/dist/admin/themes/storefront/theme.css +2 -0
- package/dist/agent/agentcms.mjs +1711 -0
- package/dist/agent/chunks/hosting-D_uj-Mt1.mjs +117 -0
- package/dist/agent/chunks/index-CrvEoyi0.mjs +3608 -0
- package/dist/agent/chunks/install-DkCCcKAO.mjs +577 -0
- package/dist/agent/chunks/plan-EYKegfhp.mjs +245 -0
- package/dist/agent/chunks/registry-lkfb_E9O.mjs +53517 -0
- package/dist/agent/chunks/siteAdd-BqcYt8nu.mjs +118 -0
- package/dist/agent/chunks/sql-wasm-GjJDiefO.mjs +10 -0
- package/dist/agent/chunks/sqljs-DjSCStJ0.mjs +20 -0
- package/dist/agent/vendor/sql-wasm.cjs +188 -0
- package/dist/theme-assets/corporate-menu.js +266 -0
- package/dist/theme-assets/corporate-posts.js +571 -0
- package/dist/theme-assets/corporate.css +2 -0
- package/dist/theme-assets/default-menu.js +266 -0
- package/dist/theme-assets/default-posts.js +378 -0
- package/dist/theme-assets/default.css +1 -0
- package/dist/theme-assets/magazine-menu.js +227 -0
- package/dist/theme-assets/magazine-posts.js +361 -0
- package/dist/theme-assets/magazine.css +2 -0
- package/dist/theme-assets/portfolio-filters.js +104 -0
- package/dist/theme-assets/portfolio-menu.js +240 -0
- package/dist/theme-assets/portfolio-posts.js +358 -0
- package/dist/theme-assets/portfolio.css +2 -0
- package/dist/theme-assets/storefront-catalog.js +504 -0
- package/dist/theme-assets/storefront-menu.js +239 -0
- package/dist/theme-assets/storefront-posts.js +404 -0
- package/dist/theme-assets/storefront.css +2 -0
- package/dist/version.json +9 -0
- package/package.json +101 -4
- package/plugin/.claude-plugin/plugin.json +16 -0
- package/plugin/skills/agentcms-install/SKILL.md +110 -0
- package/plugin/skills/agentcms-install/references/cloudflare.md +15 -0
- package/plugin/skills/agentcms-install/references/flexweg.md +11 -0
- package/plugin/skills/agentcms-install/references/github.md +67 -0
- package/plugin/skills/agentcms-install/references/gitlab.md +15 -0
- package/plugin/skills/agentcms-install/references/s3.md +81 -0
- package/plugin/skills/agentcms-manage/SKILL.md +92 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront theme — runtime catalog loader.
|
|
3
|
+
*
|
|
4
|
+
* Powers the catalog page (default `/catalog.html`). Fetches
|
|
5
|
+
* /data/products.json, renders an <article> card per product into
|
|
6
|
+
* [data-cms-catalog-grid], wires up the sidebar filters (search /
|
|
7
|
+
* categories / tags / price range / stock / sort), and re-lays out
|
|
8
|
+
* the grid via a FLIP animation when filters change.
|
|
9
|
+
*
|
|
10
|
+
* Pure vanilla JS, no external dependencies. The CatalogTemplate
|
|
11
|
+
* ships an empty grid + empty filter widgets; this loader fills them
|
|
12
|
+
* at first paint.
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
// ─── Base path ────────────────────────────────────────────────────
|
|
18
|
+
// Sites served from a sub-folder (e.g. GitHub Pages project sites)
|
|
19
|
+
// carry <meta name="x-cms-base" content="/repo/"> in <head>; no meta
|
|
20
|
+
// means the site lives at the domain root. URLs built at runtime from
|
|
21
|
+
// JSON data go through withBase() so they land under that folder.
|
|
22
|
+
var BASE = (function () {
|
|
23
|
+
var meta = document.querySelector('meta[name="x-cms-base"]');
|
|
24
|
+
var b = (meta && meta.getAttribute("content")) || "/";
|
|
25
|
+
if (b.charAt(0) !== "/") b = "/" + b;
|
|
26
|
+
if (b.charAt(b.length - 1) !== "/") b += "/";
|
|
27
|
+
return b;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
function withBase(url) {
|
|
31
|
+
if (BASE === "/" || !url) return url;
|
|
32
|
+
// Absolute URLs (http:, mailto:, data:, …), protocol-relative and
|
|
33
|
+
// fragment-only links pass through.
|
|
34
|
+
if (/^([a-z][a-z0-9+.-]*:|\/\/|#)/i.test(url)) return url;
|
|
35
|
+
if (url.indexOf(BASE) === 0 || url === BASE.slice(0, -1)) return url;
|
|
36
|
+
return BASE + (url.charAt(0) === "/" ? url.slice(1) : url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var SEARCH_DEBOUNCE_MS = 200;
|
|
40
|
+
var EMPTY_PRICE_PAIR = "—";
|
|
41
|
+
|
|
42
|
+
function fetchProducts() {
|
|
43
|
+
return fetch(withBase("/data/products.json"), { credentials: "omit", cache: "no-store" })
|
|
44
|
+
.then(function (res) {
|
|
45
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
46
|
+
return res.json();
|
|
47
|
+
})
|
|
48
|
+
.catch(function (err) {
|
|
49
|
+
console.warn("[cms] products.json load failed:", err);
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ready(fn) {
|
|
55
|
+
if (document.readyState !== "loading") fn();
|
|
56
|
+
else document.addEventListener("DOMContentLoaded", fn);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeHtml(str) {
|
|
60
|
+
if (typeof str !== "string") return "";
|
|
61
|
+
return str
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatPrice(amount, currency, locale) {
|
|
69
|
+
if (typeof amount !== "number" || isNaN(amount) || amount <= 0) return "";
|
|
70
|
+
try {
|
|
71
|
+
return new Intl.NumberFormat(locale || "en", {
|
|
72
|
+
style: "currency",
|
|
73
|
+
currency: currency || "EUR",
|
|
74
|
+
minimumFractionDigits: 2,
|
|
75
|
+
maximumFractionDigits: 2,
|
|
76
|
+
}).format(amount);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return amount.toFixed(2) + " " + (currency || "");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── State ────────────────────────────────────────────────────────
|
|
83
|
+
// Translated UI strings — populated at boot from the grid host's
|
|
84
|
+
// `data-cms-catalog-i18n` attribute (encoded server-side by the
|
|
85
|
+
// CatalogTemplate using t()). Keeps the loader language-agnostic.
|
|
86
|
+
var i18nLabels = {
|
|
87
|
+
inStock: "In stock",
|
|
88
|
+
lowStock: "Low stock",
|
|
89
|
+
outOfStock: "Out of stock",
|
|
90
|
+
onOrder: "On order",
|
|
91
|
+
productCountOne: "1 product",
|
|
92
|
+
productCount: "{count} products",
|
|
93
|
+
empty: "No products match your filters.",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function loadI18n(grid) {
|
|
97
|
+
if (!grid) return;
|
|
98
|
+
var raw = grid.getAttribute("data-cms-catalog-i18n");
|
|
99
|
+
if (!raw) return;
|
|
100
|
+
try {
|
|
101
|
+
var parsed = JSON.parse(raw);
|
|
102
|
+
for (var k in parsed) {
|
|
103
|
+
if (parsed[k]) i18nLabels[k] = parsed[k];
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.warn("[cms] catalog i18n parse failed:", e);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
var state = {
|
|
111
|
+
products: [],
|
|
112
|
+
visibleIds: [],
|
|
113
|
+
filters: {
|
|
114
|
+
search: "",
|
|
115
|
+
categories: new Set(),
|
|
116
|
+
tags: new Set(),
|
|
117
|
+
stockStatuses: new Set(),
|
|
118
|
+
priceMax: 0,
|
|
119
|
+
priceMaxAvailable: 0,
|
|
120
|
+
sort: "newest",
|
|
121
|
+
},
|
|
122
|
+
locale: document.documentElement.lang || "en",
|
|
123
|
+
currency: "EUR",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ─── Card renderer ────────────────────────────────────────────────
|
|
127
|
+
function renderCard(p) {
|
|
128
|
+
var article = document.createElement("article");
|
|
129
|
+
article.className = "storefront-catalog-card storefront-product-card flex flex-col group";
|
|
130
|
+
article.setAttribute("data-cms-catalog-id", p.id);
|
|
131
|
+
|
|
132
|
+
var imgWrap = document.createElement("div");
|
|
133
|
+
imgWrap.className = "relative aspect-[4/5] overflow-hidden rounded-2xl bg-surface-container-lowest mb-3 shadow-sm";
|
|
134
|
+
if (p.imageUrl) {
|
|
135
|
+
imgWrap.innerHTML =
|
|
136
|
+
'<a href="' + escapeHtml(withBase("/" + p.url)) + '" class="block w-full h-full">' +
|
|
137
|
+
'<img src="' + escapeHtml(withBase(p.imageUrl)) + '" alt="' + escapeHtml(p.imageAlt || p.title) + '" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" loading="lazy" />' +
|
|
138
|
+
"</a>";
|
|
139
|
+
}
|
|
140
|
+
if (p.badges && p.badges.length > 0) {
|
|
141
|
+
var badgeChip = document.createElement("span");
|
|
142
|
+
badgeChip.className = "absolute top-3 left-3 bg-secondary/90 text-on-secondary text-[11px] font-semibold px-3 py-1 rounded-full backdrop-blur uppercase tracking-widest";
|
|
143
|
+
badgeChip.textContent = p.badges[0];
|
|
144
|
+
imgWrap.appendChild(badgeChip);
|
|
145
|
+
}
|
|
146
|
+
article.appendChild(imgWrap);
|
|
147
|
+
|
|
148
|
+
var h3 = document.createElement("h3");
|
|
149
|
+
h3.className = "font-serif text-[1.125rem] text-on-surface leading-tight mb-1";
|
|
150
|
+
var titleA = document.createElement("a");
|
|
151
|
+
titleA.href = withBase("/" + p.url);
|
|
152
|
+
titleA.textContent = p.title;
|
|
153
|
+
titleA.className = "hover:text-primary transition-colors";
|
|
154
|
+
h3.appendChild(titleA);
|
|
155
|
+
article.appendChild(h3);
|
|
156
|
+
|
|
157
|
+
if (p.categoryName) {
|
|
158
|
+
var catP = document.createElement("p");
|
|
159
|
+
catP.className = "text-[12px] text-on-surface-variant uppercase tracking-widest mb-1";
|
|
160
|
+
catP.textContent = p.categoryName;
|
|
161
|
+
article.appendChild(catP);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (p.priceTTC > 0) {
|
|
165
|
+
var priceP = document.createElement("p");
|
|
166
|
+
priceP.className = "text-label-caps font-semibold";
|
|
167
|
+
var priceCurrency = p.currency || state.currency;
|
|
168
|
+
if (p.promoTTC > 0 && p.promoTTC < p.priceTTC) {
|
|
169
|
+
priceP.innerHTML =
|
|
170
|
+
'<span class="storefront-price-promo">' +
|
|
171
|
+
escapeHtml(formatPrice(p.promoTTC, priceCurrency, state.locale)) +
|
|
172
|
+
'</span><span class="storefront-price-strike">' +
|
|
173
|
+
escapeHtml(formatPrice(p.priceTTC, priceCurrency, state.locale)) +
|
|
174
|
+
"</span>";
|
|
175
|
+
} else {
|
|
176
|
+
priceP.className += " text-primary";
|
|
177
|
+
priceP.textContent = formatPrice(p.priceTTC, priceCurrency, state.locale);
|
|
178
|
+
}
|
|
179
|
+
article.appendChild(priceP);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return article;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Filtering ────────────────────────────────────────────────────
|
|
186
|
+
function applyFilters() {
|
|
187
|
+
var f = state.filters;
|
|
188
|
+
var searchTerms = f.search
|
|
189
|
+
? f.search.toLowerCase().split(/\s+/).filter(Boolean)
|
|
190
|
+
: [];
|
|
191
|
+
|
|
192
|
+
var matched = state.products.filter(function (p) {
|
|
193
|
+
if (searchTerms.length > 0) {
|
|
194
|
+
var haystack = [
|
|
195
|
+
p.title,
|
|
196
|
+
p.excerpt,
|
|
197
|
+
p.categoryName,
|
|
198
|
+
(p.tags || []).map(function (t) { return t.name; }).join(" "),
|
|
199
|
+
].join(" ").toLowerCase();
|
|
200
|
+
for (var i = 0; i < searchTerms.length; i++) {
|
|
201
|
+
if (haystack.indexOf(searchTerms[i]) === -1) return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (f.categories.size > 0 && !f.categories.has(p.categoryId)) return false;
|
|
205
|
+
if (f.tags.size > 0) {
|
|
206
|
+
var hit = false;
|
|
207
|
+
for (var j = 0; j < (p.tags || []).length; j++) {
|
|
208
|
+
if (f.tags.has(p.tags[j].id)) { hit = true; break; }
|
|
209
|
+
}
|
|
210
|
+
if (!hit) return false;
|
|
211
|
+
}
|
|
212
|
+
if (f.stockStatuses.size > 0 && !f.stockStatuses.has(p.stockStatus)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (f.priceMax > 0 && p.effectivePrice > f.priceMax) return false;
|
|
216
|
+
return true;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
matched.sort(function (a, b) {
|
|
220
|
+
switch (f.sort) {
|
|
221
|
+
case "price-asc":
|
|
222
|
+
return (a.effectivePrice || Infinity) - (b.effectivePrice || Infinity);
|
|
223
|
+
case "price-desc":
|
|
224
|
+
return (b.effectivePrice || 0) - (a.effectivePrice || 0);
|
|
225
|
+
case "newest":
|
|
226
|
+
default:
|
|
227
|
+
return b.createdAt - a.createdAt;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
state.visibleIds = matched.map(function (p) { return p.id; });
|
|
232
|
+
paintGrid(matched);
|
|
233
|
+
updateCount(matched.length);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// FLIP-animated paint. Records each card's old position, swaps to
|
|
237
|
+
// new state, measures new positions, applies inverse transforms,
|
|
238
|
+
// then animates them back to identity. Skipped on the first paint.
|
|
239
|
+
var firstPaint = true;
|
|
240
|
+
function paintGrid(matched) {
|
|
241
|
+
var grid = document.querySelector("[data-cms-catalog-grid]");
|
|
242
|
+
if (!grid) return;
|
|
243
|
+
|
|
244
|
+
if (firstPaint) {
|
|
245
|
+
grid.innerHTML = "";
|
|
246
|
+
if (matched.length === 0) {
|
|
247
|
+
grid.appendChild(emptyMessage());
|
|
248
|
+
firstPaint = false;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
var frag = document.createDocumentFragment();
|
|
252
|
+
matched.forEach(function (p) { frag.appendChild(renderCard(p)); });
|
|
253
|
+
grid.appendChild(frag);
|
|
254
|
+
firstPaint = false;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
var oldCards = Array.prototype.slice.call(
|
|
259
|
+
grid.querySelectorAll(".storefront-catalog-card"),
|
|
260
|
+
);
|
|
261
|
+
var oldRects = {};
|
|
262
|
+
oldCards.forEach(function (c) {
|
|
263
|
+
var id = c.getAttribute("data-cms-catalog-id");
|
|
264
|
+
oldRects[id] = c.getBoundingClientRect();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
var existingById = {};
|
|
268
|
+
oldCards.forEach(function (c) {
|
|
269
|
+
existingById[c.getAttribute("data-cms-catalog-id")] = c;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
grid.innerHTML = "";
|
|
273
|
+
if (matched.length === 0) {
|
|
274
|
+
grid.appendChild(emptyMessage());
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
var matchedFrag = document.createDocumentFragment();
|
|
279
|
+
matched.forEach(function (p) {
|
|
280
|
+
var node = existingById[p.id];
|
|
281
|
+
if (!node) node = renderCard(p);
|
|
282
|
+
matchedFrag.appendChild(node);
|
|
283
|
+
});
|
|
284
|
+
grid.appendChild(matchedFrag);
|
|
285
|
+
|
|
286
|
+
var newCards = Array.prototype.slice.call(
|
|
287
|
+
grid.querySelectorAll(".storefront-catalog-card"),
|
|
288
|
+
);
|
|
289
|
+
newCards.forEach(function (c) {
|
|
290
|
+
var id = c.getAttribute("data-cms-catalog-id");
|
|
291
|
+
var oldRect = oldRects[id];
|
|
292
|
+
if (!oldRect) return;
|
|
293
|
+
var newRect = c.getBoundingClientRect();
|
|
294
|
+
var dx = oldRect.left - newRect.left;
|
|
295
|
+
var dy = oldRect.top - newRect.top;
|
|
296
|
+
if (dx === 0 && dy === 0) return;
|
|
297
|
+
c.style.transition = "none";
|
|
298
|
+
c.style.transform = "translate(" + dx + "px, " + dy + "px)";
|
|
299
|
+
});
|
|
300
|
+
requestAnimationFrame(function () {
|
|
301
|
+
newCards.forEach(function (c) {
|
|
302
|
+
c.style.transition = "transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1)";
|
|
303
|
+
c.style.transform = "";
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function emptyMessage() {
|
|
309
|
+
var p = document.createElement("p");
|
|
310
|
+
p.className = "storefront-catalog-empty";
|
|
311
|
+
p.textContent = i18nLabels.empty;
|
|
312
|
+
return p;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function updateCount(n) {
|
|
316
|
+
var el = document.querySelector("[data-cms-catalog-count]");
|
|
317
|
+
if (!el) return;
|
|
318
|
+
if (n === 1) {
|
|
319
|
+
el.textContent = i18nLabels.productCountOne;
|
|
320
|
+
} else {
|
|
321
|
+
el.textContent = (i18nLabels.productCount || "{count} products").replace(
|
|
322
|
+
"{count}",
|
|
323
|
+
String(n),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Sidebar widgets ──────────────────────────────────────────────
|
|
329
|
+
function renderCategoryFilters(facets) {
|
|
330
|
+
var host = document.querySelector("[data-cms-catalog-categories-host]");
|
|
331
|
+
if (!host || !facets || !Array.isArray(facets.categories)) return;
|
|
332
|
+
if (facets.categories.length === 0) {
|
|
333
|
+
host.parentElement && host.parentElement.setAttribute("hidden", "");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
host.innerHTML = "";
|
|
337
|
+
facets.categories.forEach(function (c) {
|
|
338
|
+
var label = document.createElement("label");
|
|
339
|
+
label.className = "flex items-center gap-2 cursor-pointer hover:text-primary transition-colors";
|
|
340
|
+
var input = document.createElement("input");
|
|
341
|
+
input.type = "checkbox";
|
|
342
|
+
input.className = "storefront-filter-checkbox";
|
|
343
|
+
input.value = c.id;
|
|
344
|
+
input.addEventListener("change", function () {
|
|
345
|
+
if (input.checked) state.filters.categories.add(c.id);
|
|
346
|
+
else state.filters.categories.delete(c.id);
|
|
347
|
+
applyFilters();
|
|
348
|
+
});
|
|
349
|
+
var span = document.createElement("span");
|
|
350
|
+
span.textContent = c.name + " (" + c.count + ")";
|
|
351
|
+
label.appendChild(input);
|
|
352
|
+
label.appendChild(span);
|
|
353
|
+
host.appendChild(label);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function renderTagFilters(facets) {
|
|
358
|
+
var host = document.querySelector("[data-cms-catalog-tags-host]");
|
|
359
|
+
if (!host || !facets || !Array.isArray(facets.tags)) return;
|
|
360
|
+
if (facets.tags.length === 0) {
|
|
361
|
+
host.parentElement && host.parentElement.setAttribute("hidden", "");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
host.innerHTML = "";
|
|
365
|
+
facets.tags.forEach(function (t) {
|
|
366
|
+
var chip = document.createElement("button");
|
|
367
|
+
chip.type = "button";
|
|
368
|
+
chip.className = "storefront-filter-chip text-label-caps uppercase tracking-widest";
|
|
369
|
+
chip.textContent = t.name;
|
|
370
|
+
chip.addEventListener("click", function () {
|
|
371
|
+
if (state.filters.tags.has(t.id)) {
|
|
372
|
+
state.filters.tags.delete(t.id);
|
|
373
|
+
chip.classList.remove("is-active");
|
|
374
|
+
} else {
|
|
375
|
+
state.filters.tags.add(t.id);
|
|
376
|
+
chip.classList.add("is-active");
|
|
377
|
+
}
|
|
378
|
+
applyFilters();
|
|
379
|
+
});
|
|
380
|
+
host.appendChild(chip);
|
|
381
|
+
});
|
|
382
|
+
// Toggle the `is-overflowing` class so the fade mask shows only
|
|
383
|
+
// when the chip cloud actually scrolls. Without this check the
|
|
384
|
+
// bottom row would always look faded even on short lists.
|
|
385
|
+
requestAnimationFrame(function () {
|
|
386
|
+
if (host.scrollHeight > host.clientHeight + 1) {
|
|
387
|
+
host.classList.add("is-overflowing");
|
|
388
|
+
} else {
|
|
389
|
+
host.classList.remove("is-overflowing");
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function renderPriceRange(facets) {
|
|
395
|
+
var slider = document.querySelector("[data-cms-catalog-price-slider]");
|
|
396
|
+
var minEl = document.querySelector("[data-cms-catalog-price-min]");
|
|
397
|
+
var maxEl = document.querySelector("[data-cms-catalog-price-max]");
|
|
398
|
+
var wrap = document.querySelector("[data-cms-catalog-price]");
|
|
399
|
+
if (!slider || !facets || !facets.priceRange || facets.priceRange.max <= 0) {
|
|
400
|
+
wrap && wrap.setAttribute("hidden", "");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
var min = facets.priceRange.min;
|
|
404
|
+
var max = facets.priceRange.max;
|
|
405
|
+
state.filters.priceMaxAvailable = max;
|
|
406
|
+
state.filters.priceMax = max;
|
|
407
|
+
slider.min = String(Math.floor(min));
|
|
408
|
+
slider.max = String(Math.ceil(max));
|
|
409
|
+
slider.value = String(Math.ceil(max));
|
|
410
|
+
slider.step = "1";
|
|
411
|
+
if (minEl) minEl.textContent = formatPrice(min, state.currency, state.locale) || EMPTY_PRICE_PAIR;
|
|
412
|
+
if (maxEl) maxEl.textContent = formatPrice(max, state.currency, state.locale) || EMPTY_PRICE_PAIR;
|
|
413
|
+
slider.addEventListener("input", function () {
|
|
414
|
+
var v = parseFloat(slider.value);
|
|
415
|
+
state.filters.priceMax = isNaN(v) ? state.filters.priceMaxAvailable : v;
|
|
416
|
+
if (maxEl) maxEl.textContent = formatPrice(v, state.currency, state.locale);
|
|
417
|
+
applyFilters();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function stockLabel(s) {
|
|
422
|
+
if (s === "in-stock") return i18nLabels.inStock;
|
|
423
|
+
if (s === "low-stock") return i18nLabels.lowStock;
|
|
424
|
+
if (s === "out-of-stock") return i18nLabels.outOfStock;
|
|
425
|
+
if (s === "on-order") return i18nLabels.onOrder;
|
|
426
|
+
return s;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function renderStockFilters(facets) {
|
|
430
|
+
var host = document.querySelector("[data-cms-catalog-stock-host]");
|
|
431
|
+
if (!host || !facets || !Array.isArray(facets.stockStatuses)) return;
|
|
432
|
+
if (facets.stockStatuses.length === 0) {
|
|
433
|
+
host.parentElement && host.parentElement.setAttribute("hidden", "");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
host.innerHTML = "";
|
|
437
|
+
facets.stockStatuses.forEach(function (s) {
|
|
438
|
+
var label = document.createElement("label");
|
|
439
|
+
label.className = "flex items-center gap-2 cursor-pointer hover:text-primary transition-colors";
|
|
440
|
+
var input = document.createElement("input");
|
|
441
|
+
input.type = "checkbox";
|
|
442
|
+
input.className = "storefront-filter-checkbox";
|
|
443
|
+
input.value = s;
|
|
444
|
+
input.addEventListener("change", function () {
|
|
445
|
+
if (input.checked) state.filters.stockStatuses.add(s);
|
|
446
|
+
else state.filters.stockStatuses.delete(s);
|
|
447
|
+
applyFilters();
|
|
448
|
+
});
|
|
449
|
+
var span = document.createElement("span");
|
|
450
|
+
span.textContent = stockLabel(s);
|
|
451
|
+
label.appendChild(input);
|
|
452
|
+
label.appendChild(span);
|
|
453
|
+
host.appendChild(label);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function wireSearch() {
|
|
458
|
+
var input = document.querySelector("[data-cms-catalog-search]");
|
|
459
|
+
if (!input) return;
|
|
460
|
+
var timeout = null;
|
|
461
|
+
input.addEventListener("input", function () {
|
|
462
|
+
if (timeout) clearTimeout(timeout);
|
|
463
|
+
timeout = setTimeout(function () {
|
|
464
|
+
state.filters.search = input.value.trim();
|
|
465
|
+
applyFilters();
|
|
466
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function wireSort() {
|
|
471
|
+
var select = document.querySelector("[data-cms-catalog-sort]");
|
|
472
|
+
if (!select) return;
|
|
473
|
+
select.addEventListener("change", function () {
|
|
474
|
+
state.filters.sort = select.value;
|
|
475
|
+
applyFilters();
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ─── Boot ─────────────────────────────────────────────────────────
|
|
480
|
+
ready(function () {
|
|
481
|
+
var grid = document.querySelector("[data-cms-catalog-grid]");
|
|
482
|
+
if (!grid) return; // Page doesn't host the catalog UI — noop.
|
|
483
|
+
loadI18n(grid);
|
|
484
|
+
|
|
485
|
+
fetchProducts().then(function (blob) {
|
|
486
|
+
if (!blob || !Array.isArray(blob.products)) {
|
|
487
|
+
grid.innerHTML = "";
|
|
488
|
+
grid.appendChild(emptyMessage());
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
state.products = blob.products;
|
|
492
|
+
state.currency = blob.currency || "EUR";
|
|
493
|
+
|
|
494
|
+
renderCategoryFilters(blob.facets);
|
|
495
|
+
renderTagFilters(blob.facets);
|
|
496
|
+
renderPriceRange(blob.facets);
|
|
497
|
+
renderStockFilters(blob.facets);
|
|
498
|
+
wireSearch();
|
|
499
|
+
wireSort();
|
|
500
|
+
|
|
501
|
+
applyFilters();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
})();
|