feat: improve TaskCard and TagList components, enhance task loading logic
- Updated TaskCard to conditionally render footer elements based on available data (due date, source, completion status). - Enhanced TagList to visually indicate deleting tags and improved button styles for better UX. - Modified useTasks hook to refresh tasks only if no initial data is present, optimizing loading behavior. - Updated TagsPageClient to manage local tags and handle optimistic UI updates during tag deletion.
This commit is contained in:
@@ -186,32 +186,36 @@ export function TaskCard({ task, onDelete, onEdit, onUpdateTitle }: TaskCardProp
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer tech avec séparateur néon */}
|
||||
<div className="pt-2 border-t border-slate-700/50">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
{task.dueDate ? (
|
||||
<span className="flex items-center gap-1 text-slate-400 font-mono">
|
||||
<span className="text-cyan-400">⏰</span>
|
||||
{formatDistanceToNow(new Date(task.dueDate), {
|
||||
addSuffix: true,
|
||||
locale: fr
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-600 font-mono">--:--</span>
|
||||
)}
|
||||
{/* Footer tech avec séparateur néon - seulement si des données à afficher */}
|
||||
{(task.dueDate || (task.source && task.source !== 'manual') || task.completedAt) && (
|
||||
<div className="pt-2 border-t border-slate-700/50">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
{task.dueDate ? (
|
||||
<span className="flex items-center gap-1 text-slate-400 font-mono">
|
||||
<span className="text-cyan-400">⏰</span>
|
||||
{formatDistanceToNow(new Date(task.dueDate), {
|
||||
addSuffix: true,
|
||||
locale: fr
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{task.source !== 'manual' && task.source && (
|
||||
<Badge variant="outline" size="sm">
|
||||
{task.source}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{task.source !== 'manual' && task.source && (
|
||||
<Badge variant="outline" size="sm">
|
||||
{task.source}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{task.completedAt && (
|
||||
<span className="text-emerald-400 font-mono font-bold">✓ DONE</span>
|
||||
)}
|
||||
{task.completedAt && (
|
||||
<span className="text-emerald-400 font-mono font-bold">✓ DONE</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Tag } from '@/lib/types';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
interface TagListProps {
|
||||
tags: (Tag & { usage?: number })[];
|
||||
@@ -7,13 +6,15 @@ interface TagListProps {
|
||||
onTagDelete?: (tag: Tag) => void;
|
||||
showActions?: boolean;
|
||||
showUsage?: boolean;
|
||||
deletingTagId?: string | null;
|
||||
}
|
||||
|
||||
export function TagList({
|
||||
tags,
|
||||
onTagEdit,
|
||||
onTagDelete,
|
||||
showActions = true
|
||||
showActions = true,
|
||||
deletingTagId
|
||||
}: TagListProps) {
|
||||
if (tags.length === 0) {
|
||||
return (
|
||||
@@ -27,10 +28,15 @@ export function TagList({
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{tags.map((tag) => (
|
||||
{tags.map((tag) => {
|
||||
const isDeleting = deletingTagId === tag.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="group relative bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-all duration-200 hover:shadow-lg hover:shadow-slate-900/20 p-3"
|
||||
className={`group relative bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-all duration-200 hover:shadow-lg hover:shadow-slate-900/20 p-3 ${
|
||||
isDeleting ? 'opacity-50 pointer-events-none' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Contenu principal */}
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -57,24 +63,21 @@ export function TagList({
|
||||
{showActions && (onTagEdit || onTagDelete) && (
|
||||
<div className="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{onTagEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<button
|
||||
onClick={() => onTagEdit(tag)}
|
||||
className="h-7 px-2 text-xs bg-slate-700/80 backdrop-blur-sm border border-slate-600 hover:border-slate-500 hover:bg-slate-600"
|
||||
className="h-7 px-2 text-xs bg-slate-800/50 backdrop-blur-sm border border-slate-700 hover:border-slate-600 hover:bg-slate-700/50 rounded-md transition-all duration-200 text-slate-300 hover:text-slate-200"
|
||||
>
|
||||
✏️
|
||||
</Button>
|
||||
</button>
|
||||
)}
|
||||
{onTagDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<button
|
||||
onClick={() => onTagDelete(tag)}
|
||||
className="h-7 px-2 text-xs bg-slate-700/80 backdrop-blur-sm border border-slate-600 hover:border-red-500 hover:text-red-400 hover:bg-red-900/20"
|
||||
disabled={isDeleting}
|
||||
className="h-7 px-2 text-xs bg-slate-800/50 backdrop-blur-sm border border-slate-700 hover:border-red-500/50 hover:text-red-400 hover:bg-red-900/20 rounded-md transition-all duration-200 text-slate-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
🗑️
|
||||
</Button>
|
||||
{isDeleting ? '⏳' : '🗑️'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -85,7 +88,8 @@ export function TagList({
|
||||
style={{ backgroundColor: tag.color }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,10 +222,12 @@ export function useTasks(
|
||||
}
|
||||
}, [refreshTasks]);
|
||||
|
||||
// Charger les tâches au montage et quand les filtres changent
|
||||
// Charger les tâches au montage seulement si pas de données initiales
|
||||
useEffect(() => {
|
||||
refreshTasks();
|
||||
}, [refreshTasks]);
|
||||
if (!initialData?.tasks?.length) {
|
||||
refreshTasks();
|
||||
}
|
||||
}, [refreshTasks, initialData?.tasks?.length]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -133,7 +133,7 @@ export const tagsService = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Supprime un tag
|
||||
* Supprime un tag et toutes ses relations
|
||||
*/
|
||||
async deleteTag(id: string): Promise<void> {
|
||||
// Vérifier que le tag existe
|
||||
@@ -142,15 +142,12 @@ export const tagsService = {
|
||||
throw new Error(`Tag avec l'ID "${id}" non trouvé`);
|
||||
}
|
||||
|
||||
// Vérifier si le tag est utilisé par des tâches via la relation TaskTag
|
||||
const taskTagCount = await prisma.taskTag.count({
|
||||
// Supprimer d'abord toutes les relations TaskTag
|
||||
await prisma.taskTag.deleteMany({
|
||||
where: { tagId: id }
|
||||
});
|
||||
|
||||
if (taskTagCount > 0) {
|
||||
throw new Error(`Impossible de supprimer le tag "${existing.name}" car il est utilisé par ${taskTagCount} tâche(s)`);
|
||||
}
|
||||
|
||||
// Puis supprimer le tag lui-même
|
||||
await prisma.tag.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export class TasksService {
|
||||
}
|
||||
}
|
||||
},
|
||||
take: filters?.limit || 100,
|
||||
take: filters?.limit, // Pas de limite par défaut - récupère toutes les tâches
|
||||
skip: filters?.offset || 0,
|
||||
orderBy: [
|
||||
{ completedAt: 'desc' },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HomePageClient } from '@/components/HomePageClient';
|
||||
export default async function HomePage() {
|
||||
// SSR - Récupération des données côté serveur
|
||||
const [initialTasks, initialStats] = await Promise.all([
|
||||
tasksService.getTasks({ limit: 20 }),
|
||||
tasksService.getTasks(),
|
||||
tasksService.getTaskStats()
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Tag } from '@/lib/types';
|
||||
import { useTags } from '@/hooks/useTags';
|
||||
import { CreateTagData, UpdateTagData } from '@/clients/tags-client';
|
||||
@@ -32,11 +33,20 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
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 initiaux si pas encore chargés
|
||||
const displayTags = tags.length > 0 ? tags : 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);
|
||||
}
|
||||
}, [tags, localTags, initialTags]);
|
||||
|
||||
const handleSearch = async (query: string) => {
|
||||
setSearchQuery(query);
|
||||
if (query.trim()) {
|
||||
@@ -67,13 +77,22 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
};
|
||||
|
||||
const handleDeleteTag = async (tag: Tag) => {
|
||||
if (confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
|
||||
try {
|
||||
await deleteTag(tag.id);
|
||||
} catch (error) {
|
||||
// L'erreur est déjà gérée dans le hook
|
||||
console.error('Erreur lors de la suppression:', error);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -122,14 +141,14 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleShowPopular}
|
||||
disabled={loading}
|
||||
disabled={false}
|
||||
>
|
||||
{showPopular ? 'Tous les tags' : 'Tags populaires'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
disabled={loading}
|
||||
disabled={false}
|
||||
>
|
||||
+ Nouveau tag
|
||||
</Button>
|
||||
@@ -194,7 +213,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
{loading && displayTags.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<div className="text-slate-400">Chargement...</div>
|
||||
</div>
|
||||
@@ -215,6 +234,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
onTagEdit={handleEditTag}
|
||||
onTagDelete={handleDeleteTag}
|
||||
showUsage={true}
|
||||
deletingTagId={deletingTagId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,7 +245,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
isOpen={isCreateModalOpen}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
onSubmit={handleCreateTag}
|
||||
loading={loading}
|
||||
loading={false}
|
||||
/>
|
||||
|
||||
<TagForm
|
||||
@@ -233,7 +253,7 @@ export function TagsPageClient({ initialTags }: TagsPageClientProps) {
|
||||
onClose={() => setEditingTag(null)}
|
||||
onSubmit={handleUpdateTag}
|
||||
tag={editingTag}
|
||||
loading={loading}
|
||||
loading={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user