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.