(function ($) { "use strict"; const state = { spTimer: null, spEnabled: true, activeId: null }; function showToast(msg) { const $toast = $("#appToast"); if (!$toast.length) return; $("#toastMsg").text(msg); const t = bootstrap.Toast.getOrCreateInstance($toast[0], { delay: 2600 }); t.show(); } function smoothScrollTo(hash) { const $target = $(hash); if (!$target.length) return; $("html, body").stop(true).animate( { scrollTop: $target.offset().top - 90 }, 450 ); } function openProjectModal(projectId) { const p = (window.PROJECTS || []).find(x => x.id === projectId); if (!p) return; $("#pmTitle").text(p.title + " • " + p.year); $("#pmSummary").text(p.summary); const $tech = $("#pmTech").empty(); (p.tech || []).forEach(t => $("").addClass("tag").text(t).appendTo($tech)); const $links = $("#pmLinks").empty(); if (p.links && p.links.length) { p.links.forEach(l => { $("", { class: "btn btn-light btn-sm", href: l.url, target: "_blank", rel: "noreferrer" }).text(l.label).appendTo($links); }); } else { $("", { class: "text-white/50 text-sm" }).text("No public links yet.").appendTo($links); } const $gallery = $("#pmGallery").empty(); const imgs = Array.isArray(p.images) ? p.images : []; if (imgs.length) { imgs.forEach((url, idx) => { $("", { class: "pm-thumb", href: url, target: "_blank", rel: "noreferrer" }).append( $("", { src: url, alt: `${p.title} screenshot ${idx + 1}` }) ).appendTo($gallery); }); } else { $("
", { class: "text-white/50 small" }).text("No screenshots yet.").appendTo($gallery); } const $modal = $("#projectModal"); if ($modal.length) { const modal = bootstrap.Modal.getOrCreateInstance($modal[0]); modal.show(); } } function applyProjectFilter(tag) { tag = (tag || "all").toString().toLowerCase(); $(".chip").removeClass("active"); $(`.chip[data-filter="${tag}"]`).addClass("active"); $(".project-card").each(function () { const $c = $(this); const t = ($c.data("project-tag") || "").toString().toLowerCase(); const show = (tag === "all") || (t === tag); if (show) $c.stop(true).fadeIn(140); else $c.stop(true).fadeOut(140); }); } function animateBarsInView() { const winTop = $(window).scrollTop(); const winBottom = winTop + $(window).height(); $(".bar").each(function () { const $bar = $(this); if ($bar.data("done")) return; const top = $bar.offset().top; if (top < winBottom - 80) { const level = parseInt($bar.data("level"), 10) || 0; $bar.find(".bar-fill").css("width", Math.max(0, Math.min(level, 100)) + "%"); $bar.data("done", true); } }); } // Scrollspy (active navlink) function setActiveNav(id) { if (!id || state.activeId === id) return; state.activeId = id; $(".navlink").removeClass("is-active"); $(`.navlink[href="#${id}"]`).addClass("is-active"); } function initScrollSpy() { const ids = ["about", "projects", "stack", "gaming", "contact"]; const $sections = ids .map(id => $("#" + id)) .filter($el => $el.length); if (!$sections.length) return; const updateActive = () => { const y = $(window).scrollTop() + 120; let bestId = null; $sections.forEach($el => { const top = $el.offset().top; const bottom = top + $el.outerHeight(); if (y >= top && y < bottom) bestId = $el.attr("id"); }); if (!bestId) { $sections.forEach($el => { if (y >= $el.offset().top - 40) bestId = $el.attr("id"); }); } if (bestId) setActiveNav(bestId); }; updateActive(); $(window).on("scroll resize", updateActive); } // Spotify function renderSpotify(payload) { const $card = $("#spotifyCard"); if (!payload || !payload.ok) { $("#spStatus").text("Spotify not configured"); $(".pulse-bars").hide(); $card.removeClass("is-playing"); return; } const mode = payload.mode; // playing | recent | offline const t = payload.track || {}; $("#spTitle").text(t.title || "—"); $("#spArtist").text(t.artist || "—"); $("#spLink").attr("href", t.url || "#"); if (t.art) $("#spArt").attr("src", t.art).show(); else $("#spArt").removeAttr("src").hide(); if (mode === "playing") { $("#spStatus").text("Now Playing"); $(".pulse-bars").show(); $card.addClass("is-playing"); } else if (mode === "recent") { $("#spStatus").text("Last Played"); $(".pulse-bars").hide(); $card.removeClass("is-playing"); } else { $("#spStatus").text("Offline"); $(".pulse-bars").hide(); $card.removeClass("is-playing"); } } function fetchSpotify() { if (!state.spEnabled) return; $.ajax({ url: "/api/spotify.php", method: "GET", dataType: "json", cache: false, timeout: 8000 }) .done(renderSpotify) .fail(function () { $("#spStatus").text("Spotify unavailable"); $(".pulse-bars").hide(); }); } // Spotify Widget var SpotifyWidget = (function ($) { var cfg = { endpoint: '/api/spotify.php' }; var $widget, $song, $artist, $status, $link, $visualizer; var $artEl; // could be or
function init() { $widget = $('#spotify-widget'); $song = $('#spotify-song'); $artist = $('#spotify-artist'); $status = $('#spotify-status-text'); $link = $('#spotify-link'); $visualizer = $('.eq-visualizer'); $artEl = $('#spotify-art'); // Replace the low-contrast logo with a badge (same idea as your JS) var $logoImg = $('.spotify-logo-icon'); if ($logoImg.length) { var $badge = $('', { 'class': 'spotify-badge', 'role': 'img', 'aria-label': 'Spotify', 'text': 'Spotify' }); $logoImg.replaceWith($badge); } update(); setInterval(update, 15000); } function update() { $.ajax({ url: cfg.endpoint, method: 'GET', dataType: 'json', cache: false }) .done(function (data) { if (!data || data.ok !== true) { hide(); return; } if (data.mode === 'offline') { hide(); return; } render(data); }) .fail(function (xhr) { console.warn('Spotify widget API failed:', xhr.status, xhr.responseText); hide(); }); } function hide() { $widget.addClass('hidden'); } function render(data) { var t = (data.track || {}); $widget.removeClass('hidden'); $song.text(t.title || 'Not Playing'); $artist.text(t.artist || 'Spotify'); $link.attr('href', t.url || '#'); // album art if (t.art) { if ($artEl.prop('tagName') === 'IMG') { $artEl.attr('src', t.art); $artEl.attr('alt', (t.title || 'Track') + ' album art'); } else { $artEl.css({ backgroundImage: "url('" + t.art + "')", backgroundSize: 'cover', backgroundPosition: 'center' }); $artEl.find('i[data-feather]').remove(); } } if (data.mode === 'playing') { $status.text('Now Playing'); $visualizer.css('display', 'flex'); } else { $status.text('Last Played'); $visualizer.css('display', 'none'); } // marquee if long if ((t.title || '').length > 25) { $song.css('animation', 'marquee 10s linear infinite'); } else { $song.css('animation', 'none'); } } return { init: init }; })(jQuery); // Contact form (AJAX) function bindContactForm() { const contactEndpoint = (window.CONTACT_ENDPOINT || "/api/contact.php"); $("#contactForm").on("submit", function (e) { e.preventDefault(); const $form = $(this); const $btn = $form.find("button[type=submit]"); $btn.prop("disabled", true); const payload = $form.serialize(); $.ajax({ url: contactEndpoint, method: "POST", dataType: "json", data: payload, timeout: 12000 }) .done(handleRes) .fail(function () { showToast("Network error while sending."); }) .always(function () { $btn.prop("disabled", false); }); function handleRes(res) { if (res && res.ok) { showToast("Message sent."); $form[0].reset(); } else { showToast((res && res.error) ? res.error : "Could not send."); } } }); } // Init $(function () { // Smooth scroll $(document).on("click", "a[data-scroll], a[href^='#']", function (e) { const href = $(this).attr("href"); if (!href || href === "#") return; if (href.startsWith("#")) { e.preventDefault(); smoothScrollTo(href); // update nav immediately on click setActiveNav(href.replace("#", "")); // close offcanvas if open const $off = $("#mobileNav"); if ($off.length) bootstrap.Offcanvas.getOrCreateInstance($off[0]).hide(); } }); // Project card click $(".project-card").on("click", function () { openProjectModal($(this).data("project-id")); }); // Filter chips $(".chip").on("click", function () { applyProjectFilter($(this).data("filter")); }); // Bars animateBarsInView(); $(window).on("scroll", animateBarsInView); // Scrollspy active nav animation initScrollSpy(); // Contact bindContactForm(); // Spotify polling fetchSpotify(); state.spTimer = setInterval(fetchSpotify, 20000); }); })(jQuery);