1/** 2 * Portfolio â vanilla JS (no dependencies) 3 */ 4(function () { 5 "use strict"; 6 7 var header = document.getElementById("site-header"); 8 var navToggle = document.getElementById("nav-toggle"); 9 var mobileNav = document.getElementById("mobile-nav"); 10 var mobileBackdrop = document.getElementById("mobile-nav-backdrop"); 11 var scrollTopBtn = document.getElementById("scroll-top"); 12 var scrollPercentWrap = document.getElementById("scroll-percent"); 13 var scrollPercentRing = document.getElementById("scroll-percent-ring"); 14 var scrollPercentValue = document.getElementById("scroll-percent-value"); 15 var SCROLL_RING_CIRC = 132; // matches stroke-dasharray on .scroll-percent__ring 16 var contactForm = document.getElementById("contact-form"); 17 var projectsGrid = document.getElementById("projects-grid"); 18 var themesLoadMoreWrap = document.getElementById("themes-load-more-wrap"); 19 var themesLoadMoreBtn = document.getElementById("themes-load-more-btn"); 20 var whatsappPanel = document.getElementById("whatsapp-panel"); 21 var whatsappToggle = document.getElementById("whatsapp-toggle"); 22 var whatsappClose = document.getElementById("whatsapp-close"); 23 var whatsappForm = document.getElementById("whatsapp-form"); 24 var whatsappInput = document.getElementById("whatsapp-input"); 25 var whatsappMessages = document.getElementById("whatsapp-messages"); 26 var whatsappQuickBtns = document.querySelectorAll(".whatsapp-widget__quick-btn"); 27 var WHATSAPP_NUMBER = "8801911500125"; 28 29 var navLinks = document.querySelectorAll( 30 ".site-nav__link, .mobile-nav__link" 31 ); 32 var sections = document.querySelectorAll("section[id]"); 33 var filterBtns = document.querySelectorAll(".filter-tabs__btn"); 34 35 var currentFilter = "all"; 36 var projectsVisibleLimit = 6; 37 var projectsPerPage = 6; 38 var themeFallbackImage = 39 "assets/images/banner/hero-developer-workstation.jpeg"; 40 41 var sectionIds = Array.prototype.map.call(sections, function (s) { 42 return s.id; 43 }); 44 45 function getProjectCards() { 46 return document.querySelectorAll(".project-card"); 47 } 48 49 function cardMatchesCategory(card, filter) { 50 var cats = (card.getAttribute("data-category") || "").split(" "); 51 var isTheme = cats.indexOf("themes") !== -1; 52 53 if (filter === "all") { 54 return true; 55 } 56 if (filter === "themes" || filter === "wordpress") { 57 return isTheme; 58 } 59 if (filter === "plugins") { 60 return cats.indexOf("plugins") !== -1; 61 } 62 return cats.indexOf(filter) !== -1; 63 } 64 65 function countMatchingCards(filter) { 66 var count = 0; 67 getProjectCards().forEach(function (card) { 68 if (cardMatchesCategory(card, filter)) { 69 count++; 70 } 71 }); 72 return count; 73 } 74 75 function updateLoadMoreButton() { 76 if (!themesLoadMoreWrap) return; 77 78 var total = countMatchingCards(currentFilter); 79 themesLoadMoreWrap.hidden = projectsVisibleLimit >= total; 80 } 81 82 function applyFilter(filter) { 83 currentFilter = filter; 84 var visibleCount = 0; 85 86 getProjectCards().forEach(function (card) { 87 if (!cardMatchesCategory(card, filter)) { 88 card.classList.add("is-hidden"); 89 return; 90 } 91 92 if (visibleCount < projectsVisibleLimit) { 93 card.classList.remove("is-hidden"); 94 visibleCount++; 95 } else { 96 card.classList.add("is-hidden"); 97 } 98 }); 99 100 updateLoadMoreButton(); 101 } 102 103 function createEl(tag, className, text) { 104 var el = document.createElement(tag); 105 if (className) el.className = className; 106 if (text) el.textContent = text; 107 return el; 108 } 109 110 function renderThemeCards() { 111 if (!projectsGrid || !window.THEME_DEMOS) return; 112 113 window.THEME_DEMOS.forEach(function (theme, index) { 114 var article = createEl("article", "project-card theme-card"); 115 article.setAttribute("data-category", "themes wordpress"); 116 article.setAttribute("data-theme-index", String(index)); 117 118 var thumb = createEl("div", "project-card__thumb"); 119 var img = document.createElement("img"); 120 img.src = theme.image; 121 img.alt = theme.name + " WordPress theme demo"; 122 img.loading = "lazy"; 123 img.width = 640; 124 img.height = 400; 125 126 img.addEventListener("error", function onImgError() { 127 img.removeEventListener("error", onImgError); 128 if (img.src.indexOf(themeFallbackImage) === -1) { 129 img.src = themeFallbackImage; 130 return; 131 } 132 thumb.classList.add("project-card__thumb--fallback"); 133 thumb.setAttribute("data-label", theme.name); 134 img.style.display = "none"; 135 }); 136 137 thumb.appendChild(img); 138 139 var body = createEl("div", "project-card__body"); 140 body.appendChild( 141 createEl("span", "project-card__category", "ThemeForest · Live demo") 142 ); 143 body.appendChild(createEl("h3", "project-card__title", theme.name)); 144 body.appendChild( 145 createEl( 146 "p", 147 "project-card__desc", 148 "Premium WordPress themeâresponsive layouts, niche demo, and production-ready frontend craft." 149 ) 150 ); 151 152 var tags = createEl("ul", "project-card__tags"); 153 ["WordPress", "Theme"].forEach(function (label) { 154 var li = document.createElement("li"); 155 li.appendChild(createEl("span", "tag", label)); 156 tags.appendChild(li); 157 }); 158 body.appendChild(tags); 159 160 var actions = createEl("div", "project-card__actions"); 161 var link = createEl("a", "project-card__link project-card__link--primary", "Live site"); 162 link.href = theme.url; 163 link.target = "_blank"; 164 link.rel = "noopener noreferrer"; 165 actions.appendChild(link); 166 body.appendChild(actions); 167 168 article.appendChild(thumb); 169 article.appendChild(body); 170 projectsGrid.appendChild(article); 171 }); 172 } 173 174 function onScroll() { 175 var y = window.scrollY || document.documentElement.scrollTop; 176 177 if (header) { 178 header.classList.toggle("is-scrolled", y > 24); 179 } 180 181 if (scrollTopBtn) { 182 scrollTopBtn.classList.toggle("is-visible", y > 400); 183 } 184 185 if (scrollPercentWrap && scrollPercentRing && scrollPercentValue) { 186 var maxScroll = document.documentElement.scrollHeight - window.innerHeight; 187 var pct = maxScroll > 0 ? Math.min(100, Math.max(0, Math.round((y / maxScroll) * 100))) : 0; 188 scrollPercentValue.textContent = pct + "%"; 189 scrollPercentRing.style.strokeDashoffset = 190 SCROLL_RING_CIRC - (SCROLL_RING_CIRC * pct) / 100; 191 } 192 193 updateActiveNav(); 194 } 195 196 function updateActiveNav() { 197 var scrollPos = (window.scrollY || document.documentElement.scrollTop) + 120; 198 var current = "home"; 199 200 sectionIds.forEach(function (id) { 201 var el = document.getElementById(id); 202 if (el && el.offsetTop <= scrollPos) { 203 current = id; 204 } 205 }); 206 207 navLinks.forEach(function (link) { 208 var href = link.getAttribute("href"); 209 var isActive = href === "#" + current; 210 link.classList.toggle("is-active", isActive); 211 if (link.closest(".mobile-nav")) { 212 link.setAttribute("aria-current", isActive ? "page" : null); 213 } 214 }); 215 } 216 217 function openMobileNav() { 218 mobileNav.classList.add("is-open"); 219 mobileNav.setAttribute("aria-hidden", "false"); 220 navToggle.setAttribute("aria-expanded", "true"); 221 document.body.style.overflow = "hidden"; 222 } 223 224 function closeMobileNav() { 225 mobileNav.classList.remove("is-open"); 226 mobileNav.setAttribute("aria-hidden", "true"); 227 navToggle.setAttribute("aria-expanded", "false"); 228 document.body.style.overflow = ""; 229 } 230 231 if (navToggle) { 232 navToggle.addEventListener("click", function () { 233 if (mobileNav.classList.contains("is-open")) { 234 closeMobileNav(); 235 } else { 236 openMobileNav(); 237 } 238 }); 239 } 240 241 if (mobileBackdrop) { 242 mobileBackdrop.addEventListener("click", closeMobileNav); 243 } 244 245 navLinks.forEach(function (link) { 246 link.addEventListener("click", closeMobileNav); 247 }); 248 249 document.addEventListener("keydown", function (e) { 250 if (e.key === "Escape") { 251 closeMobileNav(); 252 closeWhatsappPanel(); 253 } 254 }); 255 256 function openWhatsappPanel() { 257 if (!whatsappPanel || !whatsappToggle) return; 258 whatsappPanel.classList.add("is-open"); 259 whatsappPanel.setAttribute("aria-hidden", "false"); 260 whatsappToggle.setAttribute("aria-expanded", "true");
261 if (whatsappInput) { 262 window.setTimeout(function () { 263 whatsappInput.focus(); 264 }, 150); 265 } 266 } 267 268 function closeWhatsappPanel() { 269 if (!whatsappPanel || !whatsappToggle) return; 270 whatsappPanel.classList.remove("is-open"); 271 whatsappPanel.setAttribute("aria-hidden", "true"); 272 whatsappToggle.setAttribute("aria-expanded", "false"); 273 } 274 275 function appendWhatsappBubble(text, type) { 276 if (!whatsappMessages) return; 277 278 var bubble = document.createElement("p"); 279 bubble.className = 280 "whatsapp-widget__bubble whatsapp-widget__bubble--" + (type || "user"); 281 bubble.textContent = text; 282 whatsappMessages.appendChild(bubble); 283 whatsappMessages.scrollTop = whatsappMessages.scrollHeight; 284 } 285 286 function sendWhatsappMessage(message) { 287 var text = (message || (whatsappInput ? whatsappInput.value : "")).trim(); 288 if (!text) return; 289 290 appendWhatsappBubble(text, "user"); 291 if (whatsappInput) { 292 whatsappInput.value = ""; 293 } 294 295 var url = 296 "https://wa.me/" + 297 WHATSAPP_NUMBER + 298 "?text=" + 299 encodeURIComponent(text); 300 window.open(url, "_blank", "noopener,noreferrer"); 301 } 302 303 if (whatsappToggle) { 304 whatsappToggle.addEventListener("click", function () { 305 if (whatsappPanel && whatsappPanel.classList.contains("is-open")) { 306 closeWhatsappPanel(); 307 } else { 308 openWhatsappPanel(); 309 } 310 }); 311 } 312 313 if (whatsappClose) { 314 whatsappClose.addEventListener("click", closeWhatsappPanel); 315 } 316 317 if (whatsappForm) { 318 whatsappForm.addEventListener("submit", function (e) { 319 e.preventDefault(); 320 sendWhatsappMessage(); 321 }); 322 } 323 324 whatsappQuickBtns.forEach(function (btn) { 325 btn.addEventListener("click", function () { 326 sendWhatsappMessage(btn.getAttribute("data-message") || ""); 327 }); 328 }); 329 330 document.querySelectorAll('a[href^="#"]').forEach(function (anchor) { 331 anchor.addEventListener("click", function (e) { 332 var targetId = this.getAttribute("href"); 333 if (!targetId || targetId === "#") return; 334 335 var target = document.querySelector(targetId); 336 if (!target) return; 337 338 e.preventDefault(); 339 var offset = header ? header.offsetHeight : 0; 340 var top = 341 target.getBoundingClientRect().top + window.pageYOffset - offset; 342 343 window.scrollTo({ top: top, behavior: "smooth" }); 344 history.pushState(null, "", targetId); 345 }); 346 }); 347 348 if (scrollTopBtn) { 349 scrollTopBtn.addEventListener("click", function () { 350 window.scrollTo({ top: 0, behavior: "smooth" }); 351 }); 352 } 353 354 filterBtns.forEach(function (btn) { 355 btn.addEventListener("click", function () { 356 var filter = btn.getAttribute("data-filter"); 357 358 filterBtns.forEach(function (b) { 359 var active = b === btn; 360 b.classList.toggle("is-active", active); 361 b.setAttribute("aria-selected", active ? "true" : "false"); 362 }); 363 364 projectsVisibleLimit = 6; 365 applyFilter(filter); 366 }); 367 }); 368 369 if (themesLoadMoreBtn) { 370 themesLoadMoreBtn.addEventListener("click", function () { 371 projectsVisibleLimit += projectsPerPage; 372 applyFilter(currentFilter); 373 }); 374 } 375 376 if (contactForm) { 377 contactForm.addEventListener("submit", function (e) { 378 e.preventDefault(); 379 if (!contactForm.checkValidity()) { 380 contactForm.reportValidity(); 381 return; 382 } 383 384 var nameEl = document.getElementById("name"); 385 var emailEl = document.getElementById("email"); 386 var messageEl = document.getElementById("message"); 387 var name = nameEl ? nameEl.value.trim() : ""; 388 var email = emailEl ? emailEl.value.trim() : ""; 389 var message = messageEl ? messageEl.value.trim() : ""; 390 var subject = encodeURIComponent("Portfolio inquiry from " + name); 391 var body = encodeURIComponent( 392 "Name: " + name + "\nEmail: " + email + "\n\n" + message 393 ); 394 395 window.location.href = 396 "mailto:[email protected]?subject=" + subject + "&body=" + body; 397 }); 398 } 399 400 renderThemeCards(); 401 applyFilter("all"); 402 403 window.addEventListener("scroll", onScroll, { passive: true }); 404 onScroll(); 405})();
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.