Files
wallabicher/web/frontend/src/components/ArticleCard.vue
Omar Sánchez Pizarro 81bf0675ed mongodb
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
2026-01-20 03:21:50 +01:00

226 lines
9.4 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="card hover:shadow-lg transition-shadow">
<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">
<div v-if="article.images && article.images.length > 0" class="w-24 h-24 sm:w-32 sm:h-32 relative">
<img
:src="article.images[0]"
:alt="article.title || 'Sin título'"
class="w-24 h-24 sm:w-32 sm:h-32 object-cover rounded-lg"
@error="($event) => handleImageError($event)"
/>
</div>
<div v-else class="w-24 h-24 sm:w-32 sm:h-32 bg-gray-200 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span class="text-gray-400 dark:text-gray-500 text-xs">Sin imagen</span>
</div>
</div>
<!-- Información del artículo -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between mb-2">
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 mb-2">
<span
class="px-2 py-1 text-xs font-semibold rounded flex-shrink-0"
:class="
article.platform === 'wallapop'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
: 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
"
>
{{ article.platform?.toUpperCase() || 'N/A' }}
</span>
<span v-if="article.username" class="px-2 py-1 text-xs font-medium rounded bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300 flex-shrink-0" title="Usuario">
👤 {{ article.username }}
</span>
<span v-if="article.worker_name" class="px-2 py-1 text-xs font-medium rounded bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300 flex-shrink-0" title="Worker">
{{ article.worker_name }}
</span>
<span v-if="article.notifiedAt" class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">
{{ formatDate(article.notifiedAt) }}
</span>
<span v-if="article.addedAt && !article.notifiedAt" class="text-xs sm:text-sm text-gray-500 dark:text-gray-400">
Añadido: {{ formatDate(article.addedAt) }}
</span>
</div>
<h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-100 mb-1 line-clamp-2" :title="article.title">
{{ article.title || 'Sin título' }}
</h3>
<div v-if="article.price !== null && article.price !== undefined" class="mb-2">
<span class="text-xl font-bold text-primary-600 dark:text-primary-400">
{{ article.price }} {{ article.currency || '€' }}
</span>
</div>
<div class="space-y-1 text-xs sm:text-sm text-gray-600 dark:text-gray-400 mb-2">
<div v-if="article.location" class="flex flex-wrap items-center">
<span class="font-medium">📍 Localidad:</span>
<span class="ml-2">{{ article.location }}</span>
</div>
<div v-if="article.allows_shipping !== null && article.allows_shipping !== undefined" class="flex flex-wrap items-center">
<span class="font-medium">🚚 Envío:</span>
<span class="ml-2">{{ article.allows_shipping ? '✅ Acepta envíos' : '❌ No acepta envíos' }}</span>
</div>
<div v-if="article.modified_at" class="flex flex-wrap items-center">
<span class="font-medium">🕒 Modificado:</span>
<span class="ml-2 break-all">{{ article.modified_at }}</span>
</div>
</div>
<p v-if="article.description" class="text-xs sm:text-sm text-gray-700 dark:text-gray-300 mb-2 overflow-hidden line-clamp-2">
{{ article.description }}
</p>
<div class="flex flex-wrap items-center gap-2 sm:gap-4 mt-3">
<a
v-if="article.url"
:href="article.url"
target="_blank"
rel="noopener noreferrer"
class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 text-xs sm:text-sm font-medium break-all"
>
🔗 Ver anuncio
</a>
<span class="text-xs text-gray-400 dark:text-gray-500 break-all">
ID: {{ article.id }}
</span>
<button
v-if="showRemoveButton"
@click="$emit('remove', article.platform, article.id)"
class="btn btn-danger text-xs sm:text-sm"
>
Eliminar
</button>
<button
v-if="!showRemoveButton && isAuthenticated && !isAdding"
@click="handleAddFavorite"
class="btn text-xs sm:text-sm flex items-center gap-1"
:class="favoriteStatus ? 'btn-secondary' : 'bg-pink-500 hover:bg-pink-600 text-white border-pink-600'"
:disabled="favoriteStatus"
:title="favoriteStatus ? 'Ya está en favoritos' : 'Añadir a favoritos'"
>
<HeartIconSolid v-if="favoriteStatus" class="w-4 h-4" />
<HeartIcon v-else class="w-4 h-4" />
{{ favoriteStatus ? 'En favoritos' : 'Añadir a favoritos' }}
</button>
<button
v-if="!showRemoveButton && isAuthenticated && isAdding"
disabled
class="btn btn-secondary text-xs sm:text-sm opacity-50 cursor-not-allowed"
>
<span class="inline-block animate-spin mr-1"></span>
Añadiendo...
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted, onUnmounted } from 'vue';
import { HeartIcon } from '@heroicons/vue/24/outline';
import { HeartIcon as HeartIconSolid } from '@heroicons/vue/24/solid';
import authService from '../services/auth';
import api from '../services/api';
const props = defineProps({
article: {
type: Object,
required: true,
},
showRemoveButton: {
type: Boolean,
default: false,
},
isFavorite: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['remove', 'added']);
const isAdding = ref(false);
const isAuthenticated = ref(false);
const favoriteStatus = ref(props.isFavorite);
// Verificar autenticación al montar y cuando cambie
function checkAuth() {
isAuthenticated.value = authService.hasCredentials();
}
function formatDate(timestamp) {
if (!timestamp) return 'N/A';
return new Date(timestamp).toLocaleString('es-ES');
}
function handleImageError(event) {
// Si la imagen falla al cargar, reemplazar con placeholder
event.target.onerror = null; // Prevenir bucle infinito
event.target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgdmlld0JveD0iMCAwIDEyOCAxMjgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik00OCA0OEg4ME04MCA4MEg0OE00OCA0OEw2NCA2NEw4MCA0OE00OCA4MEw2NCA2NE04MCA4MEw2NCA2NEw0OCA4MCIgc3Ryb2tlPSIjOUI5Q0E0IiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K';
}
async function handleAddFavorite() {
if (!isAuthenticated.value || favoriteStatus.value || isAdding.value) {
return;
}
if (!props.article.platform || !props.article.id) {
alert('Error: El artículo no tiene platform o id válidos');
return;
}
isAdding.value = true;
try {
// El backend solo necesita platform e id
const favorite = {
platform: props.article.platform,
id: String(props.article.id), // Asegurar que sea string
};
await api.addFavorite(favorite);
favoriteStatus.value = true;
// Emitir evento para que el componente padre pueda actualizar si es necesario
emit('added', props.article.platform, props.article.id);
} catch (error) {
console.error('Error añadiendo a favoritos:', error);
// El interceptor de API ya maneja el error 401 mostrando el modal de login
if (error.response?.status === 404) {
alert('El artículo no se encontró en la base de datos. Asegúrate de que el artículo esté en la lista de notificados.');
} else if (error.response?.status === 400) {
alert('Error: ' + (error.response?.data?.error || 'Datos inválidos'));
} else if (error.response?.status !== 401) {
const errorMessage = error.response?.data?.error || error.message || 'Error desconocido';
alert('Error al añadir a favoritos: ' + errorMessage);
}
} finally {
isAdding.value = false;
}
}
function handleAuthChange() {
checkAuth();
}
onMounted(() => {
checkAuth();
// Escuchar cambios en la autenticación
window.addEventListener('auth-login', handleAuthChange);
window.addEventListener('auth-logout', handleAuthChange);
});
onUnmounted(() => {
window.removeEventListener('auth-login', handleAuthChange);
window.removeEventListener('auth-logout', handleAuthChange);
});
</script>