refactor: update tag data structure and improve tag handling
- Changed `TagsResponse` and `UseTagsState` to include `usage` count in the tag data structure for better tracking. - Simplified tag initialization in `useTags` to directly use `initialData`. - Enhanced `TagsPageClient` to filter and sort tags based on usage, improving user experience in tag management. - Removed unused variables and streamlined the search functionality for better performance.
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { useTags } from '@/hooks/useTags';
|
||||
import { CreateTagData, UpdateTagData } from '@/clients/tags-client';
|
||||
import { CreateTagData } from '@/clients/tags-client';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { TagList } from '@/components/ui/TagList';
|
||||
import { TagForm } from '@/components/forms/TagForm';
|
||||
import Link from 'next/link';
|
||||
|
||||
@@ -18,44 +17,38 @@ interface TagsPageClientProps {
|
||||
export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
const {
|
||||
tags,
|
||||
popularTags,
|
||||
loading,
|
||||
error,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
getPopularTags,
|
||||
searchTags
|
||||
} = useTags();
|
||||
deleteTag
|
||||
} = useTags(initialTags as (Tag & { usage: number })[]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchResults, setSearchResults] = useState<Tag[]>([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [editingTag, setEditingTag] = useState<Tag | null>(null);
|
||||
const [showPopular, setShowPopular] = useState(false);
|
||||
const [deletingTagId, setDeletingTagId] = useState<string | null>(null);
|
||||
const [localTags, setLocalTags] = useState<Tag[]>(initialTags);
|
||||
|
||||
// Utiliser les tags du hook s'ils sont chargés, sinon les tags locaux
|
||||
const displayTags = tags.length > 0 ? tags : localTags;
|
||||
const filteredTags = searchQuery ? searchResults : displayTags;
|
||||
|
||||
// Synchroniser les tags locaux avec les tags du hook une seule fois
|
||||
React.useEffect(() => {
|
||||
if (tags.length > 0 && localTags === initialTags) {
|
||||
setLocalTags(tags);
|
||||
// Filtrer et trier les tags
|
||||
const filteredAndSortedTags = useMemo(() => {
|
||||
let filtered = tags;
|
||||
|
||||
// Filtrer par recherche
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filtered = tags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
}, [tags, localTags, initialTags]);
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
const results = await searchTags(query);
|
||||
setSearchResults(results);
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Trier par usage puis par nom
|
||||
return filtered.sort((a, b) => {
|
||||
const usageA = (a as Tag & { usage?: number }).usage || 0;
|
||||
const usageB = (b as Tag & { usage?: number }).usage || 0;
|
||||
if (usageB !== usageA) return usageB - usageA;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [tags, searchQuery]);
|
||||
|
||||
const handleCreateTag = async (data: CreateTagData) => {
|
||||
await createTag(data);
|
||||
@@ -66,7 +59,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
setEditingTag(tag);
|
||||
};
|
||||
|
||||
const handleUpdateTag = async (data: { name: string; color: string }) => {
|
||||
const handleUpdateTag = async (data: { name: string; color: string; isPinned?: boolean }) => {
|
||||
if (editingTag) {
|
||||
await updateTag({
|
||||
tagId: editingTag.id,
|
||||
@@ -77,38 +70,22 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (tag: Tag) => {
|
||||
// Suppression optimiste : retirer immédiatement de l'affichage
|
||||
setDeletingTagId(tag.id);
|
||||
|
||||
// Mettre à jour les tags locaux pour suppression immédiate
|
||||
const updatedTags = displayTags.filter(t => t.id !== tag.id);
|
||||
setLocalTags(updatedTags);
|
||||
|
||||
try {
|
||||
await deleteTag(tag.id);
|
||||
// Succès : les tags seront mis à jour par le hook
|
||||
} catch (error) {
|
||||
// En cas d'erreur, restaurer le tag dans l'affichage
|
||||
setLocalTags(prev => [...prev, tag].sort((a, b) => a.name.localeCompare(b.name)));
|
||||
console.error('Erreur lors de la suppression:', error);
|
||||
} finally {
|
||||
setDeletingTagId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowPopular = async () => {
|
||||
if (!showPopular) {
|
||||
await getPopularTags(10);
|
||||
}
|
||||
setShowPopular(!showPopular);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
{/* Header */}
|
||||
{/* Header simplifié */}
|
||||
<div className="bg-slate-900/80 backdrop-blur-sm border-b border-slate-700/50">
|
||||
<div className="container mx-auto px-6 py-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Bouton retour */}
|
||||
<Link
|
||||
@@ -127,117 +104,126 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
</Link>
|
||||
|
||||
<div className="w-3 h-3 bg-purple-400 rounded-full animate-pulse shadow-purple-400/50 shadow-lg"></div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-mono font-bold text-slate-100 tracking-wider">
|
||||
Gestion des Tags
|
||||
</h1>
|
||||
<p className="text-slate-400 mt-1 font-mono text-sm">
|
||||
Organisez et gérez vos étiquettes
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="text-xl font-mono font-bold text-slate-100 tracking-wider">
|
||||
Tags ({filteredAndSortedTags.length})
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleShowPopular}
|
||||
disabled={false}
|
||||
>
|
||||
{showPopular ? 'Tous les tags' : 'Tags populaires'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
disabled={false}
|
||||
>
|
||||
+ Nouveau tag
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Nouveau tag
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contenu principal */}
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Barre de recherche */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
|
||||
Rechercher des tags
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
placeholder="Tapez pour rechercher..."
|
||||
className="max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Statistiques */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
|
||||
<div className="text-2xl font-mono font-bold text-slate-100">
|
||||
{displayTags.length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-400 font-mono uppercase tracking-wide">
|
||||
Tags total
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
|
||||
<div className="text-2xl font-mono font-bold text-slate-100">
|
||||
{popularTags.length > 0 ? popularTags[0]?.usage || 0 : 0}
|
||||
</div>
|
||||
<div className="text-sm text-slate-400 font-mono uppercase tracking-wide">
|
||||
Plus utilisé
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800/50 rounded-lg p-4 border border-slate-700">
|
||||
<div className="text-2xl font-mono font-bold text-slate-100">
|
||||
{searchQuery ? searchResults.length : displayTags.length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-400 font-mono uppercase tracking-wide">
|
||||
{searchQuery ? 'Résultats' : 'Affichés'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages d'état */}
|
||||
{error && (
|
||||
<div className="bg-red-900/20 border border-red-500/30 rounded-lg p-4">
|
||||
<div className="text-red-400 text-sm">
|
||||
Erreur : {error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && displayTags.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-slate-400">Chargement...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Liste des tags */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-mono font-bold text-slate-200 uppercase tracking-wider">
|
||||
{showPopular
|
||||
? 'Tags populaires'
|
||||
: searchQuery
|
||||
? `Résultats pour "${searchQuery}"`
|
||||
: 'Tous les tags'
|
||||
}
|
||||
</h2>
|
||||
<TagList
|
||||
tags={showPopular ? popularTags : filteredTags}
|
||||
onTagEdit={handleEditTag}
|
||||
onTagDelete={handleDeleteTag}
|
||||
showUsage={true}
|
||||
deletingTagId={deletingTagId}
|
||||
/>
|
||||
</div>
|
||||
<div className="container mx-auto px-6 py-6">
|
||||
{/* Barre de recherche */}
|
||||
<div className="max-w-md mx-auto mb-6">
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Rechercher un tag..."
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Messages d'état */}
|
||||
{error && (
|
||||
<div className="max-w-md mx-auto mb-6 bg-red-900/20 border border-red-500/30 rounded-lg p-3 text-center">
|
||||
<div className="text-red-400 text-sm">Erreur : {error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-slate-400">Chargement...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags en grille compacte */}
|
||||
{!loading && (
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{filteredAndSortedTags.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-400">
|
||||
<div className="text-6xl mb-4">🏷️</div>
|
||||
<p className="text-lg mb-2">
|
||||
{searchQuery ? 'Aucun tag trouvé' : 'Aucun tag'}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{searchQuery ? 'Essayez un autre terme' : 'Créez votre premier tag'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredAndSortedTags.map((tag) => {
|
||||
const isDeleting = deletingTagId === tag.id;
|
||||
const usage = (tag as Tag & { usage?: number }).usage || 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
className={`relative bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-all duration-200 p-4 group ${
|
||||
isDeleting ? 'opacity-50 pointer-events-none' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Actions en overlay */}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<button
|
||||
onClick={() => handleEditTag(tag)}
|
||||
className="p-1.5 text-slate-400 hover:text-slate-200 transition-colors rounded-lg hover:bg-slate-700/80"
|
||||
title="Modifier"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTag(tag)}
|
||||
className="p-1.5 text-slate-400 hover:text-red-400 transition-colors rounded-lg hover:bg-red-900/20"
|
||||
title="Supprimer"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Contenu principal */}
|
||||
<div className="flex items-start gap-3 pr-12">
|
||||
<div
|
||||
className="w-5 h-5 rounded-full flex-shrink-0 mt-0.5"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-slate-100 font-medium truncate text-sm mb-1">
|
||||
{tag.name}
|
||||
</h3>
|
||||
<p className="text-slate-400 text-xs mb-2">
|
||||
{usage} tâche{usage > 1 ? 's' : ''}
|
||||
</p>
|
||||
{tag.isPinned && (
|
||||
<span className="inline-flex items-center text-purple-400 text-xs bg-purple-400/10 px-2 py-1 rounded-full">
|
||||
🎯 Objectif
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
|
||||
Reference in New Issue
Block a user