formha/templates/e_blog/a_all_posts.html
2025-05-03 15:47:24 -06:00

356 lines
10 KiB
HTML

{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='e_blog/a_all_posts.css') }}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
.card-img-top {
max-height: 200px;
object-fit: cover;
transition: transform 0.3s ease;
}
.card-img-top:hover {
transform: scale(1.02);
}
.card-wrapper {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.card-wrapper.visible {
opacity: 1;
transform: translateY(0);
}
#loading-indicator {
display: none;
}
</style>
<div class="container my-3">
<div class="input-group mb-3">
<!-- <input id="searchInput" type="text" autocomplete="off" /> -->
<input id="search-input" type="text" class="form-control" placeholder="Buscar por autor, título o resumen..." autocomplete="off">
<!-- Botón para limpiar -->
<button id="clear-btn" class="btn btn-outline-secondary" type="button">&times;</button>
<span class="input-group-text">
<span id="result-count">0</span> &nbsp;resultado(s)
</span>
</div>
</div>
<script>
// limpiar buscador
document.getElementById('clear-btn').addEventListener('click', function () {
const input = document.getElementById('search-input');
input.value = '';
input.dispatchEvent(new Event('input')); // Por si tienes un listener que filtra en tiempo real
});
</script>
<div id="loading-indicator" class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
</div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4" id="card-container"></div>
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center" id="pagination"></ul>
</nav>
<script>
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")
};
const CONFIG = {
url: window.location.href.replace(/\/$/, "") + "/api/posts",
postsPerPage: 12,
maxVisiblePages: 5,
searchDelay: 300
};
const state = {
allPosts: [],
filteredPosts: [],
currentPage: 1,
totalPages: 1,
searchTerm: ""
};
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: () => {
localStorage.setItem("lastPage", state.currentPage);
localStorage.setItem("lastSearch", state.searchTerm);
},
animateCards: () => {
const cards = document.querySelectorAll('.card-wrapper');
cards.forEach((card, index) => {
setTimeout(() => card.classList.add('visible'), index * 100);
});
}
};
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;
}
postsToShow.forEach(post => {
const imgSrc = post.first_img || "{{ url_for('static', filename='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">
<img src="${imgSrc}" class="card-img-top" alt="${post.title}" loading="lazy">
<div class="card-body d-flex flex-column">
<div class="mb-3">
<a href="/blog/${post.id}" class="btn btn-info stretched-link" onclick="utils.saveState()">
<h5 class="card-title">${post.title}</h5>
</a>
<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.
</small>
<p class="card-text mt-2">${post.preview}...</p>
</div>
</div>
</div>
`;
DOM.container.appendChild(card);
});
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
DOM.pagination.appendChild(pageItem(state.currentPage - 1, "«", 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
DOM.pagination.appendChild(pageItem(state.currentPage + 1, "»", state.currentPage === state.totalPages));
},
updateView: () => {
render.posts();
render.pagination();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
const search = {
filterPosts: term => {
const newSearchTerm = term.toLowerCase(); // convertir el término a minúsculas
const isSameSearch = newSearchTerm === state.searchTerm;
state.searchTerm = newSearchTerm;
if (!state.searchTerm) {
state.filteredPosts = [...state.allPosts];
} else {
state.filteredPosts = state.allPosts.filter(post =>
post._autor.toLowerCase().includes(state.searchTerm) ||
post._title.toLowerCase().includes(state.searchTerm) ||
post._preview.toLowerCase().includes(state.searchTerm)
);
}
if (!isSameSearch) {
state.currentPage = 1;
}
render.updateView();
},
init: () => {
DOM.searchInput.addEventListener("input", utils.debounce(() => {
search.filterPosts(DOM.searchInput.value);
}, CONFIG.searchDelay));
}
};
const init = {
loadPosts: () => {
render.showLoading();
fetch(CONFIG.url)
.then(response => {
if (!response.ok) throw new Error('Error en la red');
return response.json();
})
.then(data => {
state.allPosts = data.map(utils.normalizePost);
const lastSearch = localStorage.getItem("lastSearch") || "";
DOM.searchInput.value = lastSearch;
state.searchTerm = lastSearch.toLowerCase();
if (!state.searchTerm) {
state.filteredPosts = [...state.allPosts];
} else {
state.filteredPosts = state.allPosts.filter(post =>
post._autor.includes(state.searchTerm) ||
post._title.includes(state.searchTerm) ||
post._preview.includes(state.searchTerm)
);
}
const lastPage = parseInt(localStorage.getItem("lastPage")) || 1;
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
state.currentPage = Math.min(lastPage, state.totalPages || 1);
render.updateView();
})
.catch(error => {
console.error('Error:', error);
DOM.container.innerHTML = `
<div class="col-12 text-center py-5">
<h4>Error al cargar los posts</h4>
<p class="text-muted">Por favor intenta recargar la página</p>
</div>
`;
})
.finally(() => {
render.hideLoading();
});
}
};
document.addEventListener("DOMContentLoaded", () => {
init.loadPosts();
search.init();
});
</script>
{% endblock body %}
{% block js %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}