PageSourceSearch

https://artefactoschool.online/chtm/renderFiltersAndCardsWorkshop.js

js artefactoschool.online collected 2026-10-03 23:21:49 UTC 20,041 bytes, 464 lines download raw bytes

1document.addEventListener("DOMContentLoaded", () => {
2    const filtersWrapper = document.querySelector(".catalog-filters__wrapper");
3    const cardsContainer = document.querySelector(".catalog__cards");
4    const paginationTop = document.querySelector("#paginationTop");
5    const paginationBottom = document.querySelector("#paginationBottom");
6    const searchInput = document.querySelector(".catalog-toolbar__search");
7    const sortSelect = document.querySelector(".catalog-toolbar__sort");
8    const ENDPOINT = "/chtm/workshopApi";
9    function getPerPage() {
10        return window.innerWidth <= 768 ? 4 : 6;
11    }
12    let activeFilters = {};
13    let minPriceGlobal = 0;
14    let maxPriceGlobal = 0;
15    let currentMin = null;
16    let currentMax = null;
17    let searchQuery = "";
18    let sortBy = "default";
19    let currentPage = 1;
20    let totalCount = 0;
21    let filtersInitialized = false;
22    function initStateFromUrl() {
23        const params = new URLSearchParams(window.location.search);
24        const page = params.get("page");
25        const sort = params.get("sortBy");
26        const search = params.get("search");
27        const min = params.get("minPrice");
28        const max = params.get("maxPrice");
29        const filters = params.get("filters");
30        if (page)
31            currentPage = Number(page);
32        if (sort)
33            sortBy = sort;
34        if (search)
35            searchQuery = search;
36        if (min)
37            currentMin = Number(min);
38        if (max)
39            currentMax = Number(max);
40        if (filters) {
41            try {
42                activeFilters = JSON.parse(filters);
43            }
44            catch (e) {
45                console.warn("Invalid filters in URL");
46            }
47        }
48    }
49    function fixUrl(url) {
50        if (!url)
51            return null;
52        url = String(url).trim();
53        if (url.startsWith("//"))
54            return window.location.protocol + url;
55        if (/^https?:\/\//i.test(url))
56            return url;
57        return url;
58    }
59    function textOrNull(value) {
60        if (value === null || value === undefined)
61            return null;
62        const s = String(value).trim();
63        return s === "" ? null : s;
64    }
65    function debounce(fn, delay = 400) {
66        let timer;
67        return (...args) => {
68            clearTimeout(timer);
69            timer = setTimeout(() => fn(...args), delay);
70        };
71    }
72    function renderPagination(totalItems, perPage, currentPageValue) {
73        const totalPages = Math.ceil(totalItems / perPage);
74        const paginations = [paginationTop, paginationBottom].filter(Boolean);
75        paginations.forEach((pagination) => {
76            pagination.innerHTML = "";
77            if (totalPages <= 1)
78                return;
79            for (let i = 1; i <= totalPages; i++) {
80                const btn = document.createElement("button");
81                btn.textContent = i;
82                btn.className = "pagination__btn";
83                if (i === currentPageValue)
84                    btn.classList.add("active");
85                btn.addEventListener("click", ((pageNum) => {
86                    return () => {
87                        currentPage = pageNum;
88                        loadAndRender(false, true);
89                        const catalogRoot = document.querySelector(".catalog__cards");
90                        if (catalogRoot) {
91                            catalogRoot.scrollIntoView({
92                                behavior: "smooth",
93                                block: "start",
94                            });
95                        }
96                    };
97                })(i));
98                pagination.appendChild(btn);
99            }
100        });
101    }
102    function renderFilters(filters = [], filterValues = {}, minPrice = 0, maxPrice = 0) {
103        filtersWrapper.innerHTML = "";
104        minPriceGlobal = minPrice;
105        maxPriceGlobal = maxPrice;
106        if (currentMin === null)
107            currentMin = minPrice;
108        if (currentMax === null)
109            currentMax = maxPrice;
110        filters.forEach((filter) => {
111            const field = filter.field || "";
112            const titleText = (filter.title || "").replace(/:$/, "") + ":";
113            const section = document.createElement("div");
114            section.className = "catalog-filters__section";
115            section.dataset.filter = field;
116            const title = document.createElement("h3");
117            title.className = "catalog-filters__title";
118            title.textContent = titleText;
119            section.appendChild(title);
120            if (field === "price") {
121                section.classList.add("catalog-filters__section--price");
122                const slider = document.createElement("div");
123                slider.className = "catalog-filters__slider";
124                slider.innerHTML = `
125          <div class="catalog-filters__progress"></div>
126          <input type="range" class="catalog-filters__range catalog-filters__range--min" 
127                 min="${minPrice}" max="${maxPrice}" value="${currentMin}" step="1">
128          <input type="range" class="catalog-filters__range catalog-filters__range--max" 
129                 min="${minPrice}" max="${maxPrice}" value="${currentMax}" step="1">
130        `;
131                section.appendChild(slider);
132                const inputWrapper = document.createElement("div");
133                inputWrapper.className = "catalog-filters__price-input";
134                inputWrapper.innerHTML = `
135          <input type="number" class="catalog-filters__input catalog-filters__input--min" value="${currentMin}">
136          <input type="number" class="catalog-filters__input catalog-filters__input--max" value="${currentMax}">
137        `;
138                section.appendChild(inputWrapper);
139            }
140            else {
141                const list = document.createElement("ul");
142                list.className = "catalog-filters__list";
143                let values = filterValues[field] || [];
144                if (field === "technique") {
145                    console.log(field);
146                    values = [...values].sort((a, b) => {
147                        const av = String(a.value || "")
148                            .trim()
149                            .toLowerCase();
150                        const bv = String(b.value || "")
151                            .trim()
152                            .toLowerCase();
153                        return bv.localeCompare(av);
154                    });
155                }
156                values.forEach((vObj) => {
157                    const v = vObj?.value ? String(vObj.value).trim() : "";
158                    if (!v)
159                        return;
160                    const li = document.createElement("li");
161                    li.className = "catalog-filters__item";
162                    li.textContent = v;
163                    li.dataset.value = v;
164                    if (activeFilters[field]?.includes(v))
165                        li.classList.add("catalog-filters__item--selected");
166                    list.appendChild(li);
167                });
168                section.appendChild(list);
169                const VISIBLE = 4;
170                if ((values.length || 0) > VISIBLE) {
171                    Array.from(list.children).forEach((child, idx) => {
172                        if (idx >= VISIBLE)
173                            child.style.display = "none";
174                    });
175                    const btn = document.createElement("button");
176                    btn.className = "catalog-filters__see-more";
177                    btn.type = "button";
178                    btn.textContent = "See more...";
179                    btn.addEventListener("click", () => {
180                        const expanded = section.classList.toggle("expanded");
181                        btn.textContent = expanded ? "See less" : "See more...";
182                        Array.from(list.children).forEach((child, idx) => {
183                            if (idx >= VISIBLE)
184                                child.style.display = expanded ? "" : "none";
185                        });
186                    });
187                    section.appendChild(btn);
188                }
189            }
190            filtersWrapper.appendChild(section);
191        });
192        initFilters();
193    }
194    function initFilters() {
195        document.querySelectorAll(".catalog-filters__item").forEach((item) => {
196            item.addEventListener("click", () => {
197                const section = item.closest(".catalog-filters__section");
198                const field = section?.dataset.filter;
199                const value = item.dataset.value;
200                if (!field || !value)
201                    return;
202                const alreadySelected = item.classList.contains("catalog-filters__item--selected");
203                section
204                    .querySelectorAll(".catalog-filters__item--selected")
205                    .forEach((el) => el.classList.remove("catalog-filters__item--selected"));
206                if (alreadySelected) {
207                    delete activeFilters[field];
208                }
209                else {
210                    item.classList.add("catalog-filters__item--selected");
211                    activeFilters[field] = [value];
212                }
213                currentPage = 1;
214                loadAndRender(false);
215            });
216        });
217        const minInput = document.querySelector(".catalog-filters__input--min");
218        const maxInput = document.querySelector(".catalog-filters__input--max");
219        const minRange = document.querySelector(".catalog-filters__range--min");
220        const maxRange = document.querySelector(".catalog-filters__range--max");
221        const progress = document.querySelector(".catalog-filters__progress");
222        if (minRange && maxRange && progress) {
223            const updateProgress = () => {
224                const range = maxRange.max - minRange.min;
225                const left = ((minRange.value - minRange.min) / range) * 100;
226                const right = ((maxRange.value - minRange.min) / range) * 100;
227                progress.style.left = `${left}%`;
228                progress.style.right = `${100 - right}%`;
229            };
230            const updateValues = debounce(() => {
231                currentMin = parseInt(minRange.value);
232                currentMax = parseInt(maxRange.value);
233                minInput.value = currentMin;
234                maxInput.value = currentMax;
235                currentPage = 1;
236                loadAndRender(false);
237            }, 500);
238            minRange.addEventListener("input", () => {
239                updateProgress();
240                updateValues();
241            });
242            maxRange.addEventListener("input", () => {
243                updateProgress();
244                updateValues();
245            });
246            minInput.addEventListener("change", () => {
247                minRange.value = minInput.value;
248                updateProgress();
249                updateValues();
250            });
251            maxInput.addEventListener("change", () => {
252                maxRange.value = maxInput.value;
253                updateProgress();
254                updateValues();
255            });
256            updateProgress();
257        }
258        if (searchInput) {
259            searchInput.addEventListener("input", debounce((e) => {
260                searchQuery = e.target.value.trim();
261                currentPage = 1;
262                loadAndRender(false);
263            }, 500));
264        }
265        if (sortSelect) {
266            sortSelect.addEventListener("change", (e) => {
267                sortBy = e.target.value;
268                currentPage = 1;
269                loadAndRender(false);
270            });
271        }
272    }
273    const resetBtn = document.querySelector("#resetFilters");
274    if (resetBtn) {
275        resetBtn.addEventListener("click", () => {
276            activeFilters = {};
277            searchQuery = "";
278            sortBy = "default";
279            currentPage = 1;
280            currentMin = minPriceGlobal;
281            currentMax = maxPriceGlobal;
282            const searchInput = document.querySelector(".catalog-toolbar__search");
283            if (searchInput)
284                searchInput.value = "";
285            const sortSelect = document.querySelector(".catalog-toolbar__sort");
286            if (sortSelect)
287                sortSelect.value = "default";
288            filtersInitialized = false;
289            loadAndRender(true);
290        });
291    }
292    function renderCourses(courses) {
293        cardsContainer.innerHTML = "";
294        if (!courses.length) {
295            cardsContainer.innerHTML = `<p style="padding:20px;">No courses found.</p>`;
296            return;
297        }
298        courses.forEach((course) => {
299            const title = textOrNull(course.title);
300            const picture = fixUrl(course.pictureUrl);
301            const landing = textOrNull(course.landingUrl);
302            const authorName = textOrNull(course.authorName);
303            const description = textOrNull(course.description);
304            const technique = textOrNull(course.technique);
305            const paintings = textOrNull(course.paintings);
306            const lifetime = textOrNull(course.lifetime);
307            const level = textOrNull(course.level);
308            const duration = textOrNull(course.duration);
309            const language = textOrNull(course.language);
310            const price = Number(course.price) || null;
311            const offerId = textOrNull(course.offer_id);
312            const card = document.createElement("article");
313            card.className = "catalog-card";
314            if (picture) {
315                const img = document.createElement("img");
316                img.className = "catalog-card__img";
317                img.src = picture;
318                img.alt = title || "Course image";
319                img.loading = "lazy";
320                if (landing) {
321                    img.style.cursor = "pointer";
322                    img.addEventListener("click", () => window.open(landing, "_blank"));
323                }
324                card.appendChild(img);
325            }
326            const content = document.createElement("div");
327            content.className = "catalog-card__content";
328            if (authorName) {
329                const name = document.createElement("h4");
330                name.classList.add("catalog-card__author-name");
331                name.textContent = authorName;
332                content.appendChild(name);
333            }
334            if (title) {
335                const h3 = document.createElement("h3");
336                h3.className = "catalog-card__title";
337                h3.textContent = title;
338                content.appendChild(h3);
339            }
340            if (description) {
341                const p = document.createElement("p");
342                p.className = "catalog-card__descr";
343                p.innerHTML = `${description}`;
344                content.appendChild(p);
345            }
346            const meta = document.createElement("ul");
347            meta.className = "catalog-card__meta";
348            if (technique)
349                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Painting medium:</strong> ${technique}</li>`;
350            if (duration)
351                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Duration:</strong> ${duration}</li>`;
352            if (paintings)
353                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Paintings:</strong> ${paintings}</li>`;
354            if (lifetime)
355                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Access:</strong> ${lifetime}</li>`;
356            if (level)
357                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Level:</strong> ${level}</li>`;
358            if (language)
359                meta.innerHTML += `<li class="catalog-card__meta-item"><strong>Language:</strong> ${language}</li>`;
360            if (meta.children.length)
361                content.appendChild(meta);
362            if (price !== null) {
363                const priceEl = document.createElement("div");
364                priceEl.className = "catalog-card__price";
365                priceEl.textContent = `from ${price}€`;
366                content.appendChild(priceEl);
367            }
368            const actions = document.createElement("div");
369            actions.className = "catalog-card__actions";
370            const buyBtn = document.createElement("button");
371            buyBtn.className = "catalog-card__btn";
372            buyBtn.textContent = "Buy";
373            if (offerId)
374                buyBtn.dataset.offerId = offerId;
375            buyBtn.addEventListener("click", () => {
376                if (offerId && typeof window.ltAddToBasket === "function") {
377                    window.ltAddToBasket(offerId);
378                }
379                else {
380                    console.log("🛒 Added:", offerId);
381                }
382            });
383            actions.appendChild(buyBtn);
384            const readLink = document.createElement("a");
385            readLink.className = "catalog-card__btn catalog-card__btn--secondary";
386            readLink.textContent = "Read more";
387            readLink.href = landing || "#";
388            readLink.target = "_blank";
389            actions.appendChild(readLink);
390            content.appendChild(actions);
391            card.appendChild(content);
392            cardsContainer.appendChild(card);
393        });
394    }
395    function updateUrl() {
396        const params = new URLSearchParams(window.location.search);
397        params.set("page", currentPage);
398        params.set("sortBy", sortBy);
399        if (searchQuery)
400            params.set("search", searchQuery);
401        else
402            params.delete("search");
403        if (currentMin !== null)
404            params.set("minPrice", currentMin);
405        else
406            params.delete("minPrice");
407        if (currentMax !== null)
408            params.set("maxPrice", currentMax);
409        else
410            params.delete("maxPrice");
411        if (Object.keys(activeFilters).length) {
412            params.set("filters", JSON.stringify(activeFilters));
413        }
414        else {
415            params.delete("filters");
416        }
417        const newUrl = `${window.location.pathname}?${params.toString()}`;
418        window.history.replaceState(null, "", newUrl);
419    }
420    async function loadAndRender(renderFiltersFlag = true, fromPagination = false) {
421        try {
422            const params = new URLSearchParams({
423                page: currentPage,
424                perPage: getPerPage(),
425                sortBy,
426                search: searchQuery,
427                filters: JSON.stringify(activeFilters),
428            });
429            if (currentMin !== null && currentMax !== null) {
430                params.set("minPrice", currentMin);
431                params.set("maxPrice", currentMax);
432            }
433            const res = await fetch(`${ENDPOINT}?${params.toString()}`);
434            const data = await res.json();
435            totalCount = data.totalCount || 0;
436            if (renderFiltersFlag && !filtersInitialized) {
437                renderFilters(data.filters, data.filterValues, data.minPrice, data.maxPrice);
438                filtersInitialized = true;
439            }
440            if (searchInput)
441                searchInput.value = searchQuery;
442            if (sortSelect)
443                sortSelect.value = sortBy;
444            renderCourses(data.courses);
445            renderPagination(totalCount, getPerPage(), currentPage);
446            updateUrl();
447        }
448        catch (e) {
449            console.error("Error loading catalog:", e);
450        }
451    }
452    const media768 = window.matchMedia("(max-width: 768px)");
453    media768.addEventListener("change", () => {
454        currentPage = 1;
455        loadAndRender(false);
456    });
457    initStateFromUrl();
458    loadAndRender();
459    const filtersToggle = document.getElementById("filtersToggle");
460    const catalogFilters = document.getElementById("catalogFilters");
461    filtersToggle.addEventListener("click", () => {
462        catalogFilters.classList.toggle("active");
463    });
464});

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.