Add package.json for project setup and implement visual effects for new articles in ArticleCard component. Enhance Articles view to track and highlight new articles during updates.

This commit is contained in:
Omar Sánchez Pizarro
2026-01-20 19:25:28 +01:00
parent deb3dc2b31
commit d5f0ba4e03
3 changed files with 139 additions and 3 deletions

View File

@@ -1,5 +1,11 @@
<template>
<div class="card hover:shadow-lg transition-shadow">
<div
class="card hover:shadow-lg transition-all duration-300"
:class="{
'highlight-new': isNew,
'bg-primary-50 dark:bg-primary-900/20 border-primary-300 dark:border-primary-700': isNew
}"
>
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4">
<!-- Imagen del artículo -->
<div class="flex-shrink-0 self-center sm:self-start">
@@ -142,6 +148,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
isNew: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['remove', 'added']);
@@ -223,3 +233,44 @@ onUnmounted(() => {
});
</script>
<style scoped>
@keyframes highlight-pulse {
0% {
background-color: rgb(239 246 255);
box-shadow: 0 0 0 0 rgb(59 130 246 / 0.7), 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
50% {
background-color: rgb(219 234 254);
box-shadow: 0 0 20px 5px rgb(59 130 246 / 0.5), 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
100% {
background-color: rgb(239 246 255);
box-shadow: 0 0 0 0 rgb(59 130 246 / 0), 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
}
@keyframes highlight-pulse-dark {
0% {
background-color: rgb(30 58 138 / 0.3);
box-shadow: 0 0 0 0 rgb(96 165 250 / 0.7), 0 4px 6px -1px rgb(0 0 0 / 0.3);
}
50% {
background-color: rgb(30 64 175 / 0.5);
box-shadow: 0 0 20px 5px rgb(96 165 250 / 0.5), 0 10px 15px -3px rgb(0 0 0 / 0.3);
}
100% {
background-color: rgb(30 58 138 / 0.15);
box-shadow: 0 0 0 0 rgb(96 165 250 / 0), 0 4px 6px -1px rgb(0 0 0 / 0.3);
}
}
.highlight-new {
animation: highlight-pulse 2s ease-out;
border-width: 2px;
}
.dark .highlight-new {
animation: highlight-pulse-dark 2s ease-out;
}
</style>

View File

@@ -208,11 +208,12 @@
<button @click="searchQuery = ''" class="btn btn-secondary mt-4">Limpiar búsqueda</button>
</div>
<div v-else class="space-y-4">
<div v-else class="space-y-4">
<ArticleCard
v-for="article in filteredArticles"
:key="`${article.platform}-${article.id}`"
:article="article"
:is-new="newArticleIds.has(`${article.platform}-${article.id}`)"
/>
<div v-if="!searchQuery" class="flex justify-center space-x-2 mt-6">
@@ -275,6 +276,10 @@ const searchTimeout = ref(null);
const POLL_INTERVAL = 30000; // 30 segundos
const SEARCH_DEBOUNCE = 500; // 500ms de debounce para búsqueda
// Rastreo de artículos nuevos para efectos visuales
const existingArticleIds = ref(new Set());
const newArticleIds = ref(new Set());
// Facets obtenidos del backend
const facets = ref({
platforms: [],
@@ -335,9 +340,24 @@ async function loadFacets() {
async function loadArticles(reset = true, silent = false) {
// Guardar IDs existentes antes de cargar (para detectar artículos nuevos en polling)
let previousIds = new Set();
if (silent && reset) {
// Para polling automático, guardar los IDs de los artículos actuales antes de resetear
allArticles.value.forEach(article => {
previousIds.add(`${article.platform}-${article.id}`);
});
} else if (!reset) {
// Para cargar más, usar los IDs ya existentes
previousIds = new Set(existingArticleIds.value);
}
if (reset) {
offset.value = 0;
allArticles.value = [];
if (!silent) {
// Solo limpiar si no es polling silencioso
newArticleIds.value.clear();
}
}
if (!silent) {
@@ -362,6 +382,31 @@ async function loadArticles(reset = true, silent = false) {
filtered = filtered.filter(a => a.platform === selectedPlatform.value);
}
// Si es polling silencioso, detectar artículos nuevos comparando con los anteriores
if (silent && reset && previousIds.size > 0) {
newArticleIds.value.clear(); // Limpiar IDs anteriores
filtered.forEach(article => {
const articleId = `${article.platform}-${article.id}`;
if (!previousIds.has(articleId)) {
newArticleIds.value.add(articleId);
}
});
// Limpiar IDs de artículos nuevos después de 5 segundos
if (newArticleIds.value.size > 0) {
setTimeout(() => {
newArticleIds.value.clear();
}, 5000);
}
}
// Actualizar el Set de IDs existentes
if (reset) {
existingArticleIds.value.clear();
}
filtered.forEach(article => {
existingArticleIds.value.add(`${article.platform}-${article.id}`);
});
if (reset) {
allArticles.value = filtered;
offset.value = limit;