- Added tag management functionality to the `GeneralSettingsPageClient`, including filtering, sorting, and CRUD operations for tags. - Integrated a modal for creating and editing tags, improving user experience in managing task labels. - Updated the `Header` component to replace the 'Tags' link with 'Manager'. - Removed the deprecated `TagsPage` and `TagsPageClient` components to streamline the codebase. - Adjusted data fetching in `page.tsx` to include initial tags alongside user preferences.
362 lines
17 KiB
TypeScript
362 lines
17 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useMemo } from 'react';
|
||
import { UserPreferences, Tag } from '@/lib/types';
|
||
import { useTags } from '@/hooks/useTags';
|
||
import { Header } from '@/components/ui/Header';
|
||
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
|
||
import { Button } from '@/components/ui/Button';
|
||
import { Input } from '@/components/ui/Input';
|
||
import { TagForm } from '@/components/forms/TagForm';
|
||
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
|
||
import Link from 'next/link';
|
||
|
||
interface GeneralSettingsPageClientProps {
|
||
initialPreferences: UserPreferences;
|
||
initialTags: Tag[];
|
||
}
|
||
|
||
export function GeneralSettingsPageClient({ initialPreferences, initialTags }: GeneralSettingsPageClientProps) {
|
||
const {
|
||
tags,
|
||
refreshTags,
|
||
deleteTag
|
||
} = useTags(initialTags as (Tag & { usage: number })[]);
|
||
|
||
const [searchQuery, setSearchQuery] = useState('');
|
||
const [showOnlyUnused, setShowOnlyUnused] = useState(false);
|
||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||
const [editingTag, setEditingTag] = useState<Tag | null>(null);
|
||
const [deletingTagId, setDeletingTagId] = useState<string | null>(null);
|
||
|
||
// Filtrer et trier les tags
|
||
const filteredTags = useMemo(() => {
|
||
let filtered = tags;
|
||
|
||
// Filtrer par recherche
|
||
if (searchQuery.trim()) {
|
||
const query = searchQuery.toLowerCase();
|
||
filtered = filtered.filter(tag =>
|
||
tag.name.toLowerCase().includes(query)
|
||
);
|
||
}
|
||
|
||
// Filtrer pour afficher seulement les non utilisés
|
||
if (showOnlyUnused) {
|
||
filtered = filtered.filter(tag => {
|
||
const usage = (tag as Tag & { usage?: number }).usage || 0;
|
||
return usage === 0;
|
||
});
|
||
}
|
||
|
||
const sorted = 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);
|
||
});
|
||
|
||
// Limiter à 12 tags si pas de recherche ni filtre, sinon afficher tous les résultats
|
||
const hasFilters = searchQuery.trim() || showOnlyUnused;
|
||
return hasFilters ? sorted : sorted.slice(0, 12);
|
||
}, [tags, searchQuery, showOnlyUnused]);
|
||
|
||
const handleEditTag = (tag: Tag) => {
|
||
setEditingTag(tag);
|
||
};
|
||
|
||
const handleDeleteTag = async (tag: Tag) => {
|
||
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
|
||
return;
|
||
}
|
||
|
||
setDeletingTagId(tag.id);
|
||
try {
|
||
await deleteTag(tag.id);
|
||
await refreshTags();
|
||
} catch (error) {
|
||
console.error('Erreur lors de la suppression:', error);
|
||
} finally {
|
||
setDeletingTagId(null);
|
||
}
|
||
};
|
||
return (
|
||
<UserPreferencesProvider initialPreferences={initialPreferences}>
|
||
<div className="min-h-screen bg-[var(--background)]">
|
||
<Header
|
||
title="TowerControl"
|
||
subtitle="Paramètres généraux"
|
||
/>
|
||
|
||
<div className="container mx-auto px-4 py-4">
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* Breadcrumb */}
|
||
<div className="mb-4 text-sm">
|
||
<Link href="/settings" className="text-[var(--muted-foreground)] hover:text-[var(--primary)]">
|
||
Paramètres
|
||
</Link>
|
||
<span className="mx-2 text-[var(--muted-foreground)]">/</span>
|
||
<span className="text-[var(--foreground)]">Général</span>
|
||
</div>
|
||
|
||
{/* Page Header */}
|
||
<div className="mb-6">
|
||
<h1 className="text-2xl font-mono font-bold text-[var(--foreground)] mb-2">
|
||
⚙️ Paramètres généraux
|
||
</h1>
|
||
<p className="text-[var(--muted-foreground)]">
|
||
Configuration des préférences de l'interface et du comportement général
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{/* Gestion des tags */}
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||
🏷️ Gestion des tags
|
||
</h2>
|
||
<p className="text-sm text-[var(--muted-foreground)] mt-1">
|
||
Créer et organiser les étiquettes pour vos tâches
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
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>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{/* Stats des tags */}
|
||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||
<div className="text-center p-3 bg-[var(--muted)]/20 rounded">
|
||
<div className="text-xl font-bold text-[var(--foreground)]">{tags.length}</div>
|
||
<div className="text-sm text-[var(--muted-foreground)]">Tags créés</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-[var(--primary)]/10 rounded">
|
||
<div className="text-xl font-bold text-[var(--primary)]">
|
||
{tags.reduce((sum, tag) => sum + ((tag as Tag & { usage?: number }).usage || 0), 0)}
|
||
</div>
|
||
<div className="text-sm text-[var(--muted-foreground)]">Utilisations</div>
|
||
</div>
|
||
<div className="text-center p-3 bg-[var(--success)]/10 rounded">
|
||
<div className="text-xl font-bold text-[var(--success)]">
|
||
{tags.filter(tag => (tag as Tag & { usage?: number }).usage && (tag as Tag & { usage?: number }).usage! > 0).length}
|
||
</div>
|
||
<div className="text-sm text-[var(--muted-foreground)]">Actifs</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recherche et filtres */}
|
||
<div className="space-y-3 mb-4">
|
||
<Input
|
||
placeholder="Rechercher un tag..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="w-full"
|
||
/>
|
||
|
||
{/* Filtres rapides */}
|
||
<div className="flex items-center gap-3">
|
||
<Button
|
||
variant={showOnlyUnused ? "primary" : "ghost"}
|
||
size="sm"
|
||
onClick={() => setShowOnlyUnused(!showOnlyUnused)}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<span className="text-xs">⚠️</span>
|
||
Tags non utilisés ({tags.filter(tag => ((tag as Tag & { usage?: number }).usage || 0) === 0).length})
|
||
</Button>
|
||
|
||
{(searchQuery || showOnlyUnused) && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
setSearchQuery('');
|
||
setShowOnlyUnused(false);
|
||
}}
|
||
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||
>
|
||
Réinitialiser
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Liste des tags en grid */}
|
||
{filteredTags.length === 0 ? (
|
||
<div className="text-center py-8 text-[var(--muted-foreground)]">
|
||
{searchQuery && showOnlyUnused ? 'Aucun tag non utilisé trouvé avec cette recherche' :
|
||
searchQuery ? 'Aucun tag trouvé pour cette recherche' :
|
||
showOnlyUnused ? '🎉 Aucun tag non utilisé ! Tous vos tags sont actifs.' :
|
||
'Aucun tag créé'}
|
||
{!searchQuery && !showOnlyUnused && (
|
||
<div className="mt-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setIsCreateModalOpen(true)}
|
||
>
|
||
Créer votre premier tag
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{/* Grid des tags */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
{filteredTags.map((tag) => {
|
||
const usage = (tag as Tag & { usage?: number }).usage || 0;
|
||
const isUnused = usage === 0;
|
||
return (
|
||
<div
|
||
key={tag.id}
|
||
className={`p-3 rounded-lg border transition-all hover:shadow-sm ${
|
||
isUnused
|
||
? 'border-[var(--destructive)]/30 bg-[var(--destructive)]/5 hover:border-[var(--destructive)]/50'
|
||
: 'border-[var(--border)] hover:border-[var(--primary)]/50'
|
||
}`}
|
||
>
|
||
{/* Header du tag */}
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||
<div
|
||
className="w-3 h-3 rounded-full flex-shrink-0"
|
||
style={{ backgroundColor: tag.color }}
|
||
/>
|
||
<span className="font-medium text-sm truncate">{tag.name}</span>
|
||
{tag.isPinned && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-[var(--primary)]/20 text-[var(--primary)] rounded flex-shrink-0">
|
||
📌
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleEditTag(tag)}
|
||
className="h-7 w-7 p-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||
>
|
||
<svg className="w-3 h-3" 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
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleDeleteTag(tag)}
|
||
disabled={deletingTagId === tag.id}
|
||
className={`h-7 w-7 p-0 ${
|
||
isUnused
|
||
? 'text-[var(--destructive)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/20'
|
||
: 'text-[var(--muted-foreground)] hover:text-[var(--destructive)] hover:bg-[var(--destructive)]/10'
|
||
}`}
|
||
>
|
||
{deletingTagId === tag.id ? (
|
||
<svg className="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
|
||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||
</svg>
|
||
) : (
|
||
<svg className="w-3 h-3" 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>
|
||
</div>
|
||
|
||
{/* Stats et warning */}
|
||
<div className="space-y-1">
|
||
<div className={`text-xs flex items-center justify-between ${
|
||
isUnused ? 'text-[var(--destructive)]' : 'text-[var(--muted-foreground)]'
|
||
}`}>
|
||
<span>{usage} utilisation{usage !== 1 ? 's' : ''}</span>
|
||
{isUnused && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-[var(--destructive)]/20 text-[var(--destructive)] rounded">
|
||
⚠️ Non utilisé
|
||
</span>
|
||
)}
|
||
</div>
|
||
{('createdAt' in tag && (tag as Tag & { createdAt: Date }).createdAt) && (
|
||
<div className="text-xs text-[var(--muted-foreground)]">
|
||
Créé le {new Date((tag as Tag & { createdAt: Date }).createdAt).toLocaleDateString('fr-FR')}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Message si plus de tags */}
|
||
{tags.length > 12 && !searchQuery && !showOnlyUnused && (
|
||
<div className="text-center pt-2 text-sm text-[var(--muted-foreground)]">
|
||
Et {tags.length - 12} autres tags... (utilisez la recherche ou les filtres pour les voir)
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Note développement futur */}
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<div className="p-4 bg-[var(--warning)]/10 border border-[var(--warning)]/20 rounded">
|
||
<p className="text-sm text-[var(--warning)] font-medium mb-2">
|
||
🚧 Interface de configuration en développement
|
||
</p>
|
||
<p className="text-xs text-[var(--muted-foreground)]">
|
||
Les contrôles interactifs pour modifier les autres préférences seront disponibles dans une prochaine version.
|
||
Pour l'instant, les préférences sont modifiables via les boutons de l'interface principale.
|
||
</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Modals pour les tags */}
|
||
{isCreateModalOpen && (
|
||
<TagForm
|
||
isOpen={isCreateModalOpen}
|
||
onClose={() => setIsCreateModalOpen(false)}
|
||
onSuccess={async () => {
|
||
setIsCreateModalOpen(false);
|
||
await refreshTags();
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{editingTag && (
|
||
<TagForm
|
||
isOpen={!!editingTag}
|
||
tag={editingTag}
|
||
onClose={() => setEditingTag(null)}
|
||
onSuccess={async () => {
|
||
setEditingTag(null);
|
||
await refreshTags();
|
||
}}
|
||
/>
|
||
)}
|
||
</UserPreferencesProvider>
|
||
);
|
||
}
|