PageSourceSearch

https://www.theengineeringprojects.com/res/js/content/base/shop.0xhpy2s4ej.js

js theengineeringprojects.com collected 2026-10-02 03:56:44 UTC 12,934 bytes, 338 lines download raw bytes

1/* Shop preview controls. Cart and saved products stay in this browser. */
2(() => {
3  "use strict";
4  const $ = (s) => document.querySelector(s),
5    $$ = (s) => [...document.querySelectorAll(s)],
6    core = globalThis.TEPShopCore;
7  const data = JSON.parse($("#shop-data").textContent);
8  const serverDataNode = $("#shop-server-data");
9  let hasServerShopData = false;
10  let cartData = data;
11  if (serverDataNode?.textContent.trim()) {
12    try {
13      const serverData = JSON.parse(serverDataNode.textContent);
14      cartData = globalThis.TEPShopCatalog.merge(data, serverData.catalog);
15      if (Array.isArray(serverData?.products)) {
16        const templates = [...data.products];
17        const templateFor = (id) => {
18          const sum = Array.from(String(id || "")).reduce(
19            (total, character) => total + character.codePointAt(0),
20            0,
21          );
22          return templates.find(product => product.id === id) || templates[sum % templates.length] || templates[0] || {};
23        };
24        const defaultCategories = [...data.categories];
25        const categoryStyles = [
26          ["chip", "cyan"],
27          ["sensor", "green"],
28          ["box", "amber"],
29          ["book", "violet"],
30        ];
31        const liveCategories = new Map();
32        (serverData.categories || []).forEach((incoming) => {
33          if (incoming?.id) liveCategories.set(incoming.id, incoming.name || incoming.id);
34        });
35        serverData.products.forEach((incoming) => {
36          if (incoming?.category)
37            liveCategories.set(
38              incoming.category,
39              incoming.categoryName || incoming.category,
40            );
41        });
42        data.categories = [...liveCategories].map(([id, name], index) => {
43          const existing = defaultCategories.find((category) => category.id === id);
44          const [icon, tone] = categoryStyles[index % categoryStyles.length];
45          return { id, name, icon: existing?.icon || icon, tone: existing?.tone || tone };
46        });
47        data.products = serverData.products.map((incoming, index) => {
48          const template = templateFor(incoming.id);
49          const categoryId = incoming.category || template.category || "uncategorized";
50          if (!data.categories.some((category) => category.id === categoryId)) {
51            const [icon, tone] = categoryStyles[index % categoryStyles.length];
52            data.categories.push({
53              id: categoryId,
54              name: incoming.categoryName || categoryId,
55              icon,
56              tone,
57            });
58          }
59          return {
60            ...template,
61            ...incoming,
62            ...incoming.commerce,
63            category: categoryId,
64            description: incoming.description || template.description,
65            tags:
66              Array.isArray(incoming.tags) && incoming.tags.length
67                ? incoming.tags
68                : template.tags,
69          };
70        });
71        hasServerShopData = true;
72      }
73    } catch {
74      // Keep the approved preview catalog if a server payload cannot be read.
75    }
76  }
77  const keyCart = "tep.shop.cart.v1",
78    keySaved = "tep.shop.saved.v1";
79  const read = (key) => {
80    try {
81      return JSON.parse(localStorage.getItem(key));
82    } catch {
83      return null;
84    }
85  };
86  let cart = core.cartFromLocation(cartData, read(keyCart)),
87    saved = read(keySaved),
88    timer,
89    returnFocus = null,
90    dialogMode = "";
91  saved = Array.isArray(saved)
92    ? [
93        ...new Set(
94          saved.filter((id) => typeof id === "string" && cartData.products.some((p) => p.id === id)),
95        ),
96      ]
97    : [];
98  const state = {
99    query: "",
100    category: "all",
101    sort: "featured",
102    price: "",
103    type: "all",
104    stock: false,
105    saved: false,
106    page: 1,
107  };
108  if (hasServerShopData) {
109    $("#shop-category").innerHTML =
110      '<option value="all">All categories</option>' +
111      data.categories
112        .map((category) => `<option value="${core.esc(category.id)}">${core.esc(category.name)}</option>`)
113        .join("");
114    $(".shop-categories").innerHTML =
115      `<button type="button" data-category="all" aria-pressed="true">${core.icon("grid")}All products <small>${data.products.length}</small></button>` +
116      data.categories
117        .map(
118          (category) =>
119            `<button type="button" data-category="${core.esc(category.id)}" aria-pressed="false">${core.icon(category.icon, category.tone)}${core.esc(category.name)}<small>${data.products.filter((product) => product.category === category.id).length}</small></button>`,
120        )
121        .join("");
122  }
123  const dialog = $("#shop-dialog");
124  function toast(message) {
125    clearTimeout(timer);
126    $("#shop-toast").textContent = message;
127    $("#shop-toast").hidden = false;
128    timer = setTimeout(() => {
129      $("#shop-toast").hidden = true;
130    }, 4500);
131  }
132  function save(key, value) {
133    try {
134      localStorage.setItem(key, JSON.stringify(value));
135      return true;
136    } catch {
137      return false;
138    }
139  }
140  function counts() {
141    $("#shop-cart-count").textContent = cart.reduce((n, x) => n + x.qty, 0);
142    $("#shop-saved-count").textContent = saved.length;
143  }
144  function render() {
145    const all = core.select(data, state, saved),
146      pages = Math.ceil(all.length / data.pageSize);
147    state.page = Math.max(1, Math.min(state.page, pages || 1));
148    const from = (state.page - 1) * data.pageSize;
149    $("#shop-grid").innerHTML = core.cards(data, all.slice(from, from + data.pageSize), saved);
150    $("#shop-grid").hidden = !all.length;
151    $("#shop-empty").hidden = Boolean(all.length);
152    $("#shop-result-count").textContent = all.length
153      ? `Showing ${from + 1}–${Math.min(from + data.pageSize, all.length)} of ${all.length} ${state.saved ? "saved " : ""}products`
154      : "No matching products";
155    $("#shop-saved").setAttribute("aria-pressed", String(state.saved));
156    $$("[data-category]").forEach((button) =>
157      button.setAttribute("aria-pressed", String(button.dataset.category === state.category)),
158    );
159    $("#shop-reset").hidden = !(
160      state.query ||
161      state.category !== "all" ||
162      state.price ||
163      state.type !== "all" ||
164      state.stock ||
165      state.saved ||
166      state.sort !== "featured"
167    );
168    $("#shop-pagination").innerHTML =
169      pages > 1
170        ? `<button type="button" data-page="${state.page - 1}" aria-label="Previous product page"${state.page === 1 ? " disabled" : ""}>${core.svg("left")}</button>` +
171          Array.from(
172            { length: pages },
173            (_, i) =>
174              `<button type="button" data-page="${i + 1}" aria-label="Product page ${i + 1}"${state.page === i + 1 ? ' aria-current="page"' : ""}>${i + 1}</button>`,
175          ).join("") +
176          `<button type="button" data-page="${state.page + 1}" aria-label="Next product page"${state.page === pages ? " disabled" : ""}>${core.svg("arrow")}</button>`
177        : "";
178    counts();
179    window.TEPShopSelects?.sync();
180  }
181  function reset() {
182    Object.assign(state, {
183      query: "",
184      category: "all",
185      sort: "featured",
186      price: "",
187      type: "all",
188      stock: false,
189      saved: false,
190      page: 1,
191    });
192    for (const name of ["search", "category", "sort", "price", "type"])
193      $("#shop-" + name).value = {
194        search: "",
195        category: "all",
196        sort: "featured",
197        price: "",
198        type: "all",
199      }[name];
200    $("#shop-stock").checked = false;
201    render();
202  }
203  function advanced(open) {
204    $("#shop-advanced").hidden = !open;
205    $("#shop-advanced-toggle").setAttribute("aria-expanded", String(open));
206  }
207  $("#shop-filters").addEventListener("submit", (event) => event.preventDefault());
208  for (const [control, property, event] of [
209    ["search", "query", "input"],
210    ["category", "category", "change"],
211    ["sort", "sort", "change"],
212    ["price", "price", "change"],
213    ["type", "type", "change"],
214    ["stock", "stock", "change"],
215  ])
216    $("#shop-" + control).addEventListener(event, () => {
217      state[property] =
218        control === "stock" ? $("#shop-stock").checked : $("#shop-" + control).value;
219      state.page = 1;
220      render();
221    });
222  $("#shop-advanced-toggle").addEventListener("click", () => advanced($("#shop-advanced").hidden));
223  $("#shop-advanced-close").addEventListener("click", () => {
224    advanced(false);
225    $("#shop-advanced-toggle").focus();
226  });
227  $("#shop-reset").addEventListener("click", reset);
228  $("#shop-empty-reset").addEventListener("click", reset);
229  $("#shop-saved").addEventListener("click", () => {
230    state.saved = !state.saved;
231    state.page = 1;
232    render();
233  });
234  function show(mode, product) {
235    dialogMode = mode;
236    returnFocus = document.activeElement;
237    $("#shop-dialog-title").textContent = mode === "cart" ? "Your cart" : "Product details";
238    $("#shop-dialog-content").innerHTML =
239      mode === "cart" ? core.cartHtml(cartData, cart) : core.detailHtml(data, product);
240    dialog.classList.toggle("shop-dialog--cart", mode === "cart");
241    dialog.showModal();
242    $("#shop-dialog-close").focus();
243  }
244  function close() {
245    dialog.close();
246  }
247  dialog.addEventListener("close", () => {
248    dialogMode = "";
249    if (returnFocus?.isConnected) returnFocus.focus();
250    else $("#shop-cart-open").focus();
251  });
252  dialog.addEventListener("click", (event) => {
253    if (event.target === dialog) close();
254  });
255  $("#shop-dialog-close").addEventListener("click", close);
256  $("#shop-cart-open").addEventListener("click", () => show("cart"));
257  function updateCart(focusSelector) {
258    const stored = save(keyCart, cart);
259    counts();
260    if (dialogMode === "cart") {
261      $("#shop-dialog-content").innerHTML = core.cartHtml(cartData, cart);
262      ((focusSelector && $(focusSelector)) || $("#shop-dialog-close")).focus();
263    }
264    if (!stored) toast("Cart updated for this visit. Browser saving is unavailable.");
265    return stored;
266  }
267  $("#shop-main").addEventListener("click", (event) => {
268    const button = event.target.closest("button");
269    if (!button || button.disabled) return;
270    if (button.hasAttribute("data-category")) {
271      state.category = button.dataset.category;
272      $("#shop-category").value = state.category;
273      state.page = 1;
274      render();
275    }
276    if (button.hasAttribute("data-page")) {
277      state.page = Number(button.dataset.page);
278      render();
279      $("#shop-grid").setAttribute("tabindex", "-1");
280      $("#shop-grid").focus({ preventScroll: true });
281      $("#shop-grid").scrollIntoView({
282        block: "start",
283        behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
284      });
285    }
286    if (button.hasAttribute("data-view")) {
287      const p = data.products.find((p) =>
287 p.id === button.dataset.view);
288      if (p) show("product", p);
289    }
290    if (button.hasAttribute("data-save")) {
291      const id = button.dataset.save,
292        p = data.products.find((p) => p.id === id);
293      if (!p) return;
294      const was = saved.includes(id);
295      saved = was ? saved.filter((x) => x !== id) : [...saved, id];
296      const stored = save(keySaved, saved);
297      render();
298      ($(`[data-save="${id}"]`) || $("#shop-saved")).focus();
299      toast(
300        (was ? "Removed from saved products." : "Product saved.") +
301          (stored ? "" : " Saved for this visit only."),
302      );
303    }
304    if (button.hasAttribute("data-add")) {
305      const p = data.products.find((p) => p.id === button.dataset.add);
306      if (!p?.stock) return;
307      const item = cart.find((x) => x.id === p.id);
308      if (item?.qty >= core.maxQuantity(p)) {
309        toast("You can add up to the available quantity of each product.");
310        return;
311      }
312      if (item) item.qty++;
313      else cart.push({ id: p.id, qty: 1 });
314      const stored = updateCart();
315      toast(p.title + " added to cart." + (stored ? "" : " Saved for this visit only."));
316    }
317    if (button.hasAttribute("data-qty")) {
318      const item = cart.find((x) => x.id === button.dataset.qty);
319      if (!item) return;
320      item.qty = Math.min(core.maxQuantity(cartData.products.find(p => p.id === item.id)), Math.max(1, item.qty + Number(button.dataset.delta)));
321      updateCart(`[data-qty="${item.id}"][data-delta="${button.dataset.delta}"]:not(:disabled)`);
322    }
323    if (button.hasAttribute("data-remove")) {
324      cart = cart.filter((x) => x.id !== button.dataset.remove);
325      updateCart("[data-qty]");
326    }
327    if (button.hasAttribute("data-close")) close();
328  });
329  const routeCategory = location.pathname.match(/^\/shop\/category\/([^/]+)\/?$/i)?.[1];
330  const requestedCategory =
331    routeCategory || new URLSearchParams(location.search).get("category");
332  if (data.categories.some((category) => category.id === requestedCategory)) {
333    state.category = requestedCategory;
334    $("#shop-category").value = requestedCategory;
335  }
336  render();
337  core.bindCartLinks(() => cart);
338})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.