Initial commit

This commit is contained in:
root
2025-12-23 13:18:58 +02:00
commit 2ef7528ee9
36 changed files with 5983 additions and 0 deletions

9
public/js/admin.js Normal file
View File

@@ -0,0 +1,9 @@
(function($){
"use strict";
$(function(){
$(document).on("click","[data-confirm]",function(e){
const msg = $(this).data("confirm") || "Are you sure?";
if(!confirm(msg)) e.preventDefault();
});
});
})(jQuery);

383
public/js/app.js Normal file
View File

@@ -0,0 +1,383 @@
(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 => $("<span/>").addClass("tag").text(t).appendTo($tech));
const $links = $("#pmLinks").empty();
if (p.links && p.links.length) {
p.links.forEach(l => {
$("<a/>", {
class: "btn btn-light btn-sm",
href: l.url,
target: "_blank",
rel: "noreferrer"
}).text(l.label).appendTo($links);
});
} else {
$("<span/>", { 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) => {
$("<a/>", {
class: "pm-thumb",
href: url,
target: "_blank",
rel: "noreferrer"
}).append(
$("<img/>", { src: url, alt: `${p.title} screenshot ${idx + 1}` })
).appendTo($gallery);
});
} else {
$("<div/>", { 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 <img> or <div>
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 = $('<span/>', {
'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);