Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 24 additions & 297 deletions scripts/main.js
Original file line number Diff line number Diff line change
@@ -1,299 +1,3 @@

// Initialize AOS (Animate On Scroll)
AOS.init();

// Array containing contributors data
const Contributors = contributors;

// ---------- Utilities for safe rendering and URL parsing ----------
function getGithubUsernameFromUrl(possibleUrl) {
try {
if (typeof possibleUrl !== "string" || possibleUrl.trim() === "") return null;
// Accept plain usernames as well as full URLs
if (!possibleUrl.includes("/")) return possibleUrl.trim();
const url = new URL(possibleUrl);
if (url.hostname !== "github.com") return null;
const path = url.pathname.replace(/^\/+/, "").split("/");
const username = path[0] || null;
return username && username.length > 0 ? username : null;
} catch (_) {
// Handle malformed URLs like 'https://github,com/...'
const fixed = possibleUrl.replace(",", ".");
try {
const url = new URL(fixed);
if (url.hostname !== "github.com") return null;
const path = url.pathname.replace(/^\/+/, "").split("/");
const username = path[0] || null;
return username && username.length > 0 ? username : null;
} catch (__){
return null;
}
}
}

function createAvatarImg(username, eager = false) {
const img = document.createElement("img");
img.loading = eager ? "eager" : "lazy";
img.decoding = "async";
img.width = 40;
img.height = 40;
// lightweight placeholder to reserve space
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.dataset.src = username
? `https://avatars.githubusercontent.com/${username}`
: "https://avatars.githubusercontent.com/ghost";
if (eager) {
img.src = img.dataset.src;
img.removeAttribute("data-src");
img.setAttribute("fetchpriority", "high");
}
img.alt = username ? `${username}'s avatar` : "avatar";
return img;
}

function createContributorAnchor(item, eager = false) {
const anchor = document.createElement("a");
anchor.className = "box-item";
const username = getGithubUsernameFromUrl(item.username);
const href = typeof item.username === "string" ? item.username : "";
if (href) {
anchor.setAttribute("href", href);
anchor.setAttribute("target", "_blank");
anchor.setAttribute("rel", "noopener noreferrer");
}

const nameSpan = document.createElement("span");
nameSpan.textContent = item.fullname || "Anonymous";
anchor.appendChild(nameSpan);

const img = createAvatarImg(username, eager);
anchor.appendChild(img);
return anchor;
}

// Variables
const searchbox = document.getElementById("search");
let searchResult = null;
// Pagination state
let pageSize = 60;
let currentPage = 1;

// Get the current year dynamically
const currentYear = new Date().getFullYear();

// Function to set the current year in the HTML
function setCurrentYear() {
const yearElements = [
// { id: "current-year", defaultValue: currentYear },
{ id: "current-year-title", defaultValue: currentYear },
{ id: "current-year-footer", defaultValue: currentYear },
{ id: "current-year-copyright", defaultValue: currentYear },
];

yearElements.forEach((element) => {
const el = document.getElementById(element.id);
if (el) {
el.textContent = element.defaultValue;
} else {
console.warn(`Element with ID '${element.id}' not found in the DOM.`);
}
});

// Set the document title with the current year
document.title = `Hacktoberfest ${currentYear} - Contributors`;
}

// Call the function to set the current year
setCurrentYear();

/**
* Filters contributors based on the search string.
* @param {string} str - The search string.
* @param {Array} array - The array of contributors.
* @returns {Array} - The filtered list of contributors.
*/
function filterUsers(str = "ContributorName", array) {
const inputString = typeof str === "string" ? str.toLowerCase() : "";
if (str === "") return "Cannot be empty, please enter a name";
return array.filter((item) => {
const fullName = item.fullname || "";
return fullName.toLowerCase().includes(inputString);
});
}

/**
* Renders the contributors on the page.
* @param {Array} array - The array of contributors to render.
* @param {Object} options - render options
* @param {boolean} options.paginate - whether to paginate the array
*/
function render(array, options = { paginate: true }) {
const container = document.getElementById("contributors");
if (!container) {
console.warn("Contributors container not found");
return;
}
const list = options.paginate
? array.slice(0, currentPage * pageSize)
: array;
list.forEach((item, index) => {
// Eager-load the first row or two on initial page for faster perceived load
const shouldEagerLoad = currentPage === 1 && index < 12;
const anchor = createContributorAnchor(item, shouldEagerLoad);
anchor.setAttribute("id", item.id);
container.appendChild(anchor);
});
// After rendering batch, invoke lazy loading for avatars
setupLazyLoadImages();
// And ensure infinite scroll sentinel is available
setupLoadMoreOnScroll();
}

// Load contributors after document loads.
render(contributors, { paginate: true });

/**
* Loads more contributors when "Load More" button is clicked.
*/
function loadMore() {
const container = document.getElementById("contributors");
if (!container) return;
const totalPages = Math.ceil(contributors.length / pageSize);
if (currentPage >= totalPages) {
render(contributors, { paginate: true });
} else {
currentPage += 1;
container.innerHTML = "<div class='text-center' id='loading'>Loading...</div>";
render(contributors, { paginate: true });
const loading = document.getElementById("loading");
if (loading) loading.setAttribute("hidden", true);
if (currentPage >= totalPages) {
const loadMoreEl = document.getElementById("loadMore");
if (loadMoreEl) loadMoreEl.setAttribute("hidden", true);
}
}
}

// Event listener for "Load More" button
const loadMoreBtn = document.getElementById("loadMore");
if (loadMoreBtn) {
loadMoreBtn.addEventListener("click", loadMore);
}

// Add avatars to existing contributor links (in case initial render occurred earlier)
document.querySelectorAll("a.box-item").forEach((con) => {
const username = getGithubUsernameFromUrl(con.getAttribute("href"));
const hasImg = con.querySelector("img");
if (!hasImg) {
con.appendChild(createAvatarImg(username));
}
});
setupLazyLoadImages();
setupLoadMoreOnScroll();

// -------- Lazy loading avatars with IntersectionObserver --------
function setupLazyLoadImages() {
const images = document.querySelectorAll("a.box-item img[data-src]");
if (!("IntersectionObserver" in window)) {
images.forEach((img) => {
img.src = img.dataset.src;
img.removeAttribute("data-src");
});
return;
}
const imgObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute("data-src");
observer.unobserve(img);
}
});
}, { rootMargin: "300px 0px", threshold: 0.01 });
images.forEach((img) => imgObserver.observe(img));
}

// -------- Infinite scroll: auto load more when near bottom --------
function setupLoadMoreOnScroll() {
const loadMoreBtn = document.getElementById("loadMore");
if (!loadMoreBtn) return;
let sentinel = document.getElementById("load-more-sentinel");
if (!sentinel) {
sentinel = document.createElement("div");
sentinel.id = "load-more-sentinel";
sentinel.style.height = "1px";
const container = document.getElementById("contributors");
if (container) container.appendChild(sentinel);
}
if (!("IntersectionObserver" in window)) return;
const totalPages = Math.ceil(contributors.length / pageSize);
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (currentPage < totalPages && (!searchbox || searchbox.value === "")) {
loadMore();
}
}
});
}, { rootMargin: "200px 0px", threshold: 0 });
observer.observe(sentinel);
}

function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

// Event listener for the search box with debounce
if (searchbox) {
searchbox.addEventListener(
"keyup",
debounce(async (e) => {
const loadMoreEl = document.getElementById("loadMore");
if (loadMoreEl) {
if (searchbox.value !== "") loadMoreEl.classList.add("hidden");
else loadMoreEl.classList.remove("hidden");
}

searchResult = await filterUsers(e.target.value, contributors);
const container = document.getElementById("contributors");
if (!container) return;
container.innerHTML = e.target.value !== "" ? "<div class='text-center' id='loading'>Loading...</div>" : "";

if (e.target.value !== "") {
searchResult.forEach((item) => {
const anchor = createContributorAnchor(item);
container.appendChild(anchor);
});
} else {
// Reset pagination when clearing search
currentPage = 1;
render(contributors, { paginate: true });
}

const loading = document.getElementById("loading");
if (loading) loading.setAttribute("hidden", true);
}, 200)
);
}

/* Back-to-top button functionality */
const backToTopButton = document.querySelector("#back-to-top-btn");

window.addEventListener("scroll", scrollFunction);
function scrollFunction() {
if (window.pageYOffset > 300) {
if (!backToTopButton.classList.contains("btnEntrance")) {
backToTopButton.classList.remove("btnExit");
backToTopButton.classList.add("btnEntrance");
backToTopButton.style.display = "block";
}
} else {
if (backToTopButton.classList.contains("btnEntrance")) {
backToTopButton.classList.remove("btnEntrance");
backToTopButton.classList.add("btnExit");
setTimeout(function () {
Expand All @@ -311,7 +15,9 @@ function smoothScrollBackToTop() {
const distance = targetPosition - startPosition;
const duration = 750;
let start = null;

window.requestAnimationFrame(step);

function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
Expand Down Expand Up @@ -340,4 +46,25 @@ $(".tdnn").click(function () {
// Display live stats with the dynamic year
document.getElementById(
"stats"
).innerHTML = `You guys are awesome, we have again passed the GitHub rate limit this hour. <a href="https://github.com/fineanmol/Hacktoberfest${currentYear}" target="_blank">Here</a> is a link to check out our repo's live stats.`;
).innerHTML = `You guys are awesome, we have again passed the GitHub rate limit this hour. <a href="https://github.com/fineanmol/Hacktoberfest" target="_blank">Here</a> is a link to check out our repo's live stats.`;

// ============================================================
// Contributor Sort Functionality - Issue #8533
// ============================================================
// TODO: Implement alphabetical sorting for contributor cards
// This function will be triggered by a sort button in the UI
// to reorder contributors alphabetically by name

/**
* Sorts contributor cards alphabetically by name
* @param {string} order - Sort order: 'asc' for ascending, 'desc' for descending
* @returns {void}
*/
function sortContributorsAlphabetically(order = 'asc') {
// TODO: Select all contributor card elements from the DOM
// TODO: Extract contributor names and sort them
// TODO: Reorder the DOM elements based on sorted order
// TODO: Apply animation or transition effects if needed

console.log(`Sorting contributors in ${order} order - functionality to be implemented`);
}