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

40
package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "wallamonitor",
"version": "1.0.0",
"description": "Scripts genéricos para Wallamonitor",
"scripts": {
"deploy:remote": "ssh root@10.10.11.5 'cd /root/wallabicher && git pull && docker compose build && docker compose up -d'",
"deploy:remote:pull": "ssh root@10.10.11.5 'cd /root/wallabicher && git pull'",
"deploy:remote:build": "ssh root@10.10.11.5 'cd /root/wallabicher && docker compose build'",
"deploy:remote:up": "ssh root@10.10.11.5 'cd /root/wallabicher && docker compose up -d'",
"deploy:remote:down": "ssh root@10.10.11.5 'cd /root/wallabicher && docker compose down'",
"deploy:remote:logs": "ssh root@10.10.11.5 'cd /root/wallabicher && docker compose logs -f'",
"deploy:remote:status": "ssh root@10.10.11.5 'cd /root/wallabicher && docker compose ps'",
"ssh:remote": "ssh root@10.10.11.5",
"docker:build": "docker compose build",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"docker:logs": "docker compose logs -f",
"docker:ps": "docker compose ps",
"docker:restart": "docker compose restart",
"docker:clean": "docker compose down -v && docker system prune -f",
"backend:dev": "cd web/backend && npm run dev",
"backend:start": "cd web/backend && npm start",
"frontend:dev": "cd web/frontend && npm run dev",
"frontend:build": "cd web/frontend && npm run build",
"frontend:preview": "cd web/frontend && npm run preview",
"install:all": "cd web/backend && npm install && cd ../frontend && npm install",
"git:status": "git status",
"git:pull": "git pull",
"git:push": "git push",
"git:commit": "git add . && git commit -m"
},
"keywords": [
"wallamonitor",
"scripts",
"deployment"
],
"author": "",
"license": "MIT"
}

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;