356 lines
10 KiB
HTML
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">×</button>
|
|
|
|
<span class="input-group-text">
|
|
<span id="result-count">0</span> 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 %} |