Enhance Articles.vue with improved search and filter functionality. Added search input, active filter chips, and clear filters button. Refactored layout for better user experience.
This commit is contained in:
@@ -10,60 +10,163 @@
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300 ml-1">({{ currentUser }})</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:space-x-4 flex-wrap">
|
||||
<select
|
||||
v-model="selectedPlatform"
|
||||
@change="loadArticles"
|
||||
class="input text-sm sm:text-base"
|
||||
style="width: 100%; min-width: 150px;"
|
||||
>
|
||||
<option value="">Todas las plataformas</option>
|
||||
<option value="wallapop">Wallapop</option>
|
||||
<option value="vinted">Vinted</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="isAdmin"
|
||||
v-model="selectedUsername"
|
||||
@change="loadArticles"
|
||||
class="input text-sm sm:text-base"
|
||||
style="width: 100%; min-width: 150px;"
|
||||
>
|
||||
<option value="">Todos los usuarios</option>
|
||||
<option v-for="username in availableUsernames" :key="username" :value="username">
|
||||
{{ username }}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-model="selectedWorker"
|
||||
@change="loadArticles"
|
||||
class="input text-sm sm:text-base"
|
||||
style="width: 100%; min-width: 150px;"
|
||||
>
|
||||
<option value="">Todos los workers</option>
|
||||
<option v-for="worker in availableWorkers" :key="worker" :value="worker">
|
||||
{{ worker }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="loadArticles" class="btn btn-primary whitespace-nowrap">
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Búsqueda y Filtros -->
|
||||
<div class="card p-4 mb-4">
|
||||
<!-- Campo de búsqueda -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<MagnifyingGlassIcon class="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
Búsqueda
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Buscar artículos por título, descripción, precio, localidad..."
|
||||
class="input pr-10 pl-10"
|
||||
@input="searchQuery = $event.target.value"
|
||||
/>
|
||||
<MagnifyingGlassIcon class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<span v-if="searching" class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<div class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div>
|
||||
</span>
|
||||
<button
|
||||
v-else-if="searchQuery"
|
||||
@click="searchQuery = ''"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
title="Limpiar búsqueda"
|
||||
>
|
||||
<XMarkIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<FunnelIcon class="w-5 h-5 text-primary-600" />
|
||||
Filtros
|
||||
<span v-if="hasActiveFilters" class="ml-2 px-2 py-0.5 bg-primary-600 text-white rounded-full text-xs font-semibold">
|
||||
{{ activeFiltersCount }}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
v-if="hasActiveFilters"
|
||||
@click="clearAllFilters"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
<XMarkIcon class="w-4 h-4 mr-1" />
|
||||
Limpiar todo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
<div class="flex flex-col">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<ServerIcon class="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
Plataforma
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedPlatform"
|
||||
@change="loadArticles"
|
||||
class="input text-sm w-full"
|
||||
>
|
||||
<option value="">Todas las plataformas</option>
|
||||
<option value="wallapop">Wallapop</option>
|
||||
<option value="vinted">Vinted</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="isAdmin" class="flex flex-col">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<UserIcon class="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
Usuario
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedUsername"
|
||||
@change="loadArticles"
|
||||
class="input text-sm w-full"
|
||||
>
|
||||
<option value="">Todos los usuarios</option>
|
||||
<option v-for="username in availableUsernames" :key="username" :value="username">
|
||||
{{ username }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
|
||||
<BriefcaseIcon class="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
Worker
|
||||
</label>
|
||||
<select
|
||||
v-model="selectedWorker"
|
||||
@change="loadArticles"
|
||||
class="input text-sm w-full"
|
||||
>
|
||||
<option value="">Todos los workers</option>
|
||||
<option v-for="worker in availableWorkers" :key="worker" :value="worker">
|
||||
{{ worker }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<button @click="loadArticles" class="btn btn-primary whitespace-nowrap">
|
||||
<ArrowPathIcon class="w-4 h-4 mr-1" />
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo de búsqueda -->
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Buscar artículos por título, descripción, precio, localidad..."
|
||||
class="input pr-10"
|
||||
@input="searchQuery = $event.target.value"
|
||||
/>
|
||||
<span v-if="searching" class="absolute right-3 top-1/2 transform -translate-y-1/2">
|
||||
<div class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div>
|
||||
<!-- Filtros activos (chips) -->
|
||||
<div v-if="hasActiveFilters" class="flex flex-wrap gap-2 mt-3 mb-4">
|
||||
<span
|
||||
v-if="selectedPlatform"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full text-sm font-medium"
|
||||
>
|
||||
<ServerIcon class="w-3.5 h-3.5" />
|
||||
{{ selectedPlatform === 'wallapop' ? 'Wallapop' : 'Vinted' }}
|
||||
<button
|
||||
@click="selectedPlatform = ''; loadArticles()"
|
||||
class="ml-1 hover:text-primary-900 dark:hover:text-primary-100"
|
||||
title="Eliminar filtro"
|
||||
>
|
||||
<XMarkIcon class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-else-if="searchQuery" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 cursor-pointer hover:text-gray-600" @click="searchQuery = ''">
|
||||
✕
|
||||
<span
|
||||
v-if="selectedUsername"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full text-sm font-medium"
|
||||
>
|
||||
<UserIcon class="w-3.5 h-3.5" />
|
||||
{{ selectedUsername }}
|
||||
<button
|
||||
@click="selectedUsername = ''; loadArticles()"
|
||||
class="ml-1 hover:text-primary-900 dark:hover:text-primary-100"
|
||||
title="Eliminar filtro"
|
||||
>
|
||||
<XMarkIcon class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedWorker"
|
||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full text-sm font-medium"
|
||||
>
|
||||
<BriefcaseIcon class="w-3.5 h-3.5" />
|
||||
{{ selectedWorker }}
|
||||
<button
|
||||
@click="selectedWorker = ''; loadArticles()"
|
||||
class="ml-1 hover:text-primary-900 dark:hover:text-primary-100"
|
||||
title="Eliminar filtro"
|
||||
>
|
||||
<XMarkIcon class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,6 +227,15 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import api from '../services/api';
|
||||
import authService from '../services/auth';
|
||||
import ArticleCard from '../components/ArticleCard.vue';
|
||||
import {
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
ServerIcon,
|
||||
UserIcon,
|
||||
BriefcaseIcon,
|
||||
ArrowPathIcon,
|
||||
MagnifyingGlassIcon
|
||||
} from '@heroicons/vue/24/outline';
|
||||
|
||||
const currentUser = ref(authService.getUsername() || null);
|
||||
const isAdmin = ref(false);
|
||||
@@ -183,6 +295,26 @@ const filteredArticles = computed(() => {
|
||||
return allArticles.value;
|
||||
});
|
||||
|
||||
// Computed para filtros activos
|
||||
const hasActiveFilters = computed(() => {
|
||||
return !!(selectedPlatform.value || selectedUsername.value || selectedWorker.value);
|
||||
});
|
||||
|
||||
const activeFiltersCount = computed(() => {
|
||||
let count = 0;
|
||||
if (selectedPlatform.value) count++;
|
||||
if (selectedUsername.value) count++;
|
||||
if (selectedWorker.value) count++;
|
||||
return count;
|
||||
});
|
||||
|
||||
function clearAllFilters() {
|
||||
selectedPlatform.value = '';
|
||||
selectedUsername.value = '';
|
||||
selectedWorker.value = '';
|
||||
loadArticles();
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadArticles(reset = true, silent = false) {
|
||||
|
||||
Reference in New Issue
Block a user