262 lines
7.8 KiB
JavaScript
262 lines
7.8 KiB
JavaScript
// DOM Elements
|
|
const DOM = {
|
|
container: document.getElementById("card-container"),
|
|
pagination: document.getElementById("pagination"),
|
|
searchInput: document.getElementById("search-input"),
|
|
loading: document.getElementById("loading-indicator"),
|
|
resultCount: document.getElementById("result-count")
|
|
};
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
postsPerPage: 12,
|
|
maxVisiblePages: 5,
|
|
searchDelay: 300
|
|
};
|
|
|
|
// Application State
|
|
const state = {
|
|
allPosts: [],
|
|
filteredPosts: [],
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
searchTerm: ""
|
|
};
|
|
|
|
// Utility Functions
|
|
const utils = {
|
|
debounce: (func, delay) => {
|
|
let timeout;
|
|
return (...args) => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
};
|
|
},
|
|
|
|
normalizePost: post => ({
|
|
...post,
|
|
_autor: post.autor.toLowerCase(),
|
|
_title: post.title.toLowerCase(),
|
|
_preview: post.preview.toLowerCase()
|
|
}),
|
|
|
|
saveState: () => {
|
|
const searchState = {
|
|
page: state.currentPage,
|
|
term: state.searchTerm
|
|
};
|
|
localStorage.setItem("searchState", JSON.stringify(searchState));
|
|
},
|
|
|
|
loadState: () => {
|
|
const savedState = JSON.parse(localStorage.getItem("searchState")) || {};
|
|
state.currentPage = savedState.page || 1;
|
|
state.searchTerm = savedState.term || "";
|
|
DOM.searchInput.value = state.searchTerm;
|
|
},
|
|
|
|
animateCards: () => {
|
|
const cards = document.querySelectorAll('.card-wrapper');
|
|
let index = 0;
|
|
|
|
const animate = () => {
|
|
if (index < cards.length) {
|
|
cards[index].classList.add('visible');
|
|
index++;
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
};
|
|
|
|
// Rendering Functions
|
|
const render = {
|
|
showLoading: () => {
|
|
DOM.loading.style.display = 'block';
|
|
DOM.container.style.opacity = '0.5';
|
|
},
|
|
|
|
hideLoading: () => {
|
|
DOM.loading.style.display = 'none';
|
|
DOM.container.style.opacity = '1';
|
|
},
|
|
|
|
posts: () => {
|
|
DOM.container.innerHTML = "";
|
|
|
|
const start = (state.currentPage - 1) * CONFIG.postsPerPage;
|
|
const end = start + CONFIG.postsPerPage;
|
|
const postsToShow = state.filteredPosts.slice(start, end);
|
|
|
|
if (postsToShow.length === 0) {
|
|
DOM.container.innerHTML = `
|
|
<div class="col-12 text-center py-5">
|
|
<h4>No se encontraron resultados</h4>
|
|
<p class="text-muted">Intenta con otros términos de búsqueda</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
postsToShow.forEach(post => {
|
|
const imgSrc = post.first_img || 'static/y_img/other/no_img.png';
|
|
const dateUpdate = post.author_updated ? `<i class="bi bi-arrow-repeat"></i> ${post.author_updated}<br>` : "";
|
|
|
|
const card = document.createElement("div");
|
|
card.className = "col card-wrapper";
|
|
card.innerHTML = `
|
|
<div class="card h-100">
|
|
<a href="/blog/${post.id}" class="stretched-link" onclick="utils.saveState()">
|
|
<img src="${imgSrc}" class="card-img-top" alt="${post.title}" loading="lazy">
|
|
</a>
|
|
<div class="card-body d-flex flex-column">
|
|
<div class="mb-3">
|
|
<h5 class="card-title">${post.title}</h5>
|
|
<small class="text-muted d-block mt-2">
|
|
<i class="bi bi-file-person-fill"></i> ${post.autor}<br>
|
|
<i class="bi bi-calendar-week"></i> ${post.author_creation}<br>
|
|
${dateUpdate}
|
|
<i class="bi bi-clock"></i> ${post.read_time_min} min(s) de lectura.
|
|
</small>
|
|
<p class="card-text mt-2">${post.preview}...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
fragment.appendChild(card);
|
|
});
|
|
|
|
DOM.container.appendChild(fragment);
|
|
utils.animateCards();
|
|
},
|
|
|
|
pagination: () => {
|
|
DOM.pagination.innerHTML = "";
|
|
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
|
DOM.resultCount.textContent = state.filteredPosts.length;
|
|
|
|
const pageItem = (page, label = null, disabled = false, active = false) => {
|
|
const li = document.createElement("li");
|
|
li.className = `page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}`;
|
|
const a = document.createElement("a");
|
|
a.className = "page-link";
|
|
a.href = "#";
|
|
a.textContent = label || page;
|
|
if (!disabled && !active) {
|
|
a.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
state.currentPage = page;
|
|
utils.saveState();
|
|
render.updateView();
|
|
});
|
|
}
|
|
li.appendChild(a);
|
|
return li;
|
|
};
|
|
|
|
const pageDots = () => {
|
|
const li = document.createElement("li");
|
|
li.className = "page-item disabled";
|
|
li.innerHTML = `<span class="page-link">...</span>`;
|
|
return li;
|
|
};
|
|
|
|
// Botón anterior
|
|
if (state.currentPage > 1) {
|
|
DOM.pagination.appendChild(pageItem(state.currentPage - 1, "«"));
|
|
}
|
|
|
|
const startPage = Math.max(1, state.currentPage - Math.floor(CONFIG.maxVisiblePages / 2));
|
|
const endPage = Math.min(state.totalPages, startPage + CONFIG.maxVisiblePages - 1);
|
|
|
|
if (startPage > 1) {
|
|
DOM.pagination.appendChild(pageItem(1));
|
|
if (startPage > 2) DOM.pagination.appendChild(pageDots());
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
DOM.pagination.appendChild(pageItem(i, null, false, i === state.currentPage));
|
|
}
|
|
|
|
if (endPage < state.totalPages) {
|
|
if (endPage < state.totalPages - 1) DOM.pagination.appendChild(pageDots());
|
|
DOM.pagination.appendChild(pageItem(state.totalPages));
|
|
}
|
|
|
|
// Botón siguiente
|
|
if (state.currentPage < state.totalPages) {
|
|
DOM.pagination.appendChild(pageItem(state.currentPage + 1, "»"));
|
|
}
|
|
},
|
|
|
|
updateView: () => {
|
|
render.posts();
|
|
render.pagination();
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
};
|
|
|
|
// Search Functionality
|
|
const search = {
|
|
filterPostsByTerm: (posts, term) => {
|
|
if (!term) return [...posts];
|
|
const loweredTerm = term.toLowerCase();
|
|
return posts.filter(post =>
|
|
post._autor.includes(loweredTerm) ||
|
|
post._title.includes(loweredTerm) ||
|
|
post._preview.includes(loweredTerm)
|
|
);
|
|
},
|
|
|
|
filterPosts: term => {
|
|
const newSearchTerm = term.toLowerCase();
|
|
if (newSearchTerm === state.searchTerm && newSearchTerm !== "") return;
|
|
|
|
state.searchTerm = newSearchTerm;
|
|
state.filteredPosts = search.filterPostsByTerm(state.allPosts, state.searchTerm);
|
|
state.currentPage = 1;
|
|
render.updateView();
|
|
},
|
|
|
|
init: () => {
|
|
DOM.searchInput.addEventListener("input", utils.debounce(() => {
|
|
search.filterPosts(DOM.searchInput.value);
|
|
}, CONFIG.searchDelay));
|
|
}
|
|
};
|
|
|
|
// Initialization
|
|
const init = {
|
|
loadPosts: () => {
|
|
// Ocultar el indicador de carga rápidamente ya que los datos ya están disponibles
|
|
setTimeout(() => {
|
|
render.hideLoading();
|
|
}, 100);
|
|
|
|
// Normalizar los posts y cargarlos en el estado
|
|
state.allPosts = allPosts.map(utils.normalizePost);
|
|
utils.loadState();
|
|
state.filteredPosts = search.filterPostsByTerm(state.allPosts, state.searchTerm);
|
|
render.updateView();
|
|
}
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
init.loadPosts();
|
|
search.init();
|
|
});
|
|
|
|
// Limpiar buscador
|
|
document.getElementById('clear-btn').addEventListener('click', function () {
|
|
const input = document.getElementById('search-input');
|
|
input.value = '';
|
|
state.searchTerm = ''; // resetear el estado
|
|
state.filteredPosts = [...state.allPosts]; // mostrar todo de nuevo
|
|
state.currentPage = 1;
|
|
render.updateView(); // actualizar la vista
|
|
localStorage.removeItem("searchState"); // opcional: limpiar localStorage
|
|
}); |