Files
towercontrol/src/components/notes/NotesList.tsx

258 lines
11 KiB
TypeScript

'use client';
import { useState, useMemo } from 'react';
import { Note } from '@/services/notes';
import { Search, Plus, Calendar, Trash2, Tags, List } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { Tag } from '@/lib/types';
interface NotesListProps {
notes: Note[];
onSelectNote: (note: Note) => void;
onCreateNote: () => void;
onDeleteNote: (noteId: string) => void;
selectedNoteId?: string;
isLoading?: boolean;
availableTags?: Tag[];
}
export function NotesList({
notes,
onSelectNote,
onCreateNote,
onDeleteNote,
selectedNoteId,
isLoading = false,
availableTags = [],
}: NotesListProps) {
const [searchQuery, setSearchQuery] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(
null
);
const [groupByTags, setGroupByTags] = useState(true);
// Filter notes based on search query
const filteredNotes = notes.filter(
(note) =>
note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
note.content.toLowerCase().includes(searchQuery.toLowerCase())
);
// Group notes by tags
const groupedNotes = useMemo(() => {
if (!groupByTags) {
return { 'Toutes les notes': filteredNotes };
}
const groups: { [key: string]: Note[] } = {};
// Notes avec tags
filteredNotes.forEach((note) => {
if (note.tags && note.tags.length > 0) {
note.tags.forEach((tag) => {
if (!groups[tag]) {
groups[tag] = [];
}
groups[tag].push(note);
});
} else {
// Notes sans tags
if (!groups['Sans tags']) {
groups['Sans tags'] = [];
}
groups['Sans tags'].push(note);
}
});
// Trier les groupes par nom de tag
const sortedGroups: { [key: string]: Note[] } = {};
Object.keys(groups)
.sort()
.forEach((key) => {
sortedGroups[key] = groups[key];
});
return sortedGroups;
}, [filteredNotes, groupByTags]);
const handleDeleteClick = (noteId: string, e: React.MouseEvent) => {
e.stopPropagation();
setShowDeleteConfirm(noteId);
};
const handleDeleteConfirm = (noteId: string) => {
onDeleteNote(noteId);
setShowDeleteConfirm(null);
};
const handleDeleteCancel = () => {
setShowDeleteConfirm(null);
};
const getNoteTitle = (content: string): string => {
// Extract title from first line, removing markdown headers
const firstLine = content.split('\n')[0] || '';
return firstLine.replace(/^#+\s*/, '').trim() || 'Sans titre';
};
return (
<div className="flex flex-col h-full bg-[var(--card)]/40 backdrop-blur-md border-r border-[var(--border)]/60 relative before:absolute before:inset-0 before:bg-gradient-to-br before:from-[color-mix(in_srgb,var(--primary)_8%,transparent)] before:via-[color-mix(in_srgb,var(--primary)_4%,transparent)] before:to-transparent before:opacity-80 before:pointer-events-none">
{/* Header */}
<div className="p-4 border-b border-[var(--border)]">
<div className="space-y-3">
<button
onClick={onCreateNote}
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-[var(--primary)]/85 hover:bg-[var(--primary)] text-[var(--primary-foreground)] rounded-md text-sm font-medium transition-all duration-200 shadow-lg shadow-[var(--primary)]/20 hover:shadow-xl hover:shadow-[var(--primary)]/30"
>
<Plus className="w-4 h-4" />
Nouvelle note
</button>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
{/* Search */}
<div className="relative flex-1">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--muted-foreground)]" />
<input
type="text"
placeholder="Rechercher une note ou un tag"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-[var(--border)]/60 rounded-md bg-[var(--card)]/40 backdrop-blur-sm text-[var(--foreground)] placeholder-[var(--muted-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]/50 focus:border-[var(--primary)]/50 focus:bg-[var(--card)]/60 transition-all duration-200"
/>
</div>
<button
onClick={() => setGroupByTags(!groupByTags)}
className={`flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-200 border border-[var(--border)]/50 bg-[var(--card)]/40 backdrop-blur-sm sm:w-auto ${
groupByTags
? 'ring-1 ring-[var(--primary)]/30 bg-[var(--primary)]/15 text-[var(--primary)]'
: 'text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--card)]/60'
}`}
title={groupByTags ? 'Vue par liste' : 'Vue par tags'}
>
{groupByTags ? (
<Tags className="w-3 h-3" />
) : (
<List className="w-3 h-3" />
)}
<span className="tracking-wide uppercase">
{groupByTags ? 'Tags' : 'Liste'}
</span>
</button>
</div>
</div>
</div>
{/* Notes List */}
<div className="flex-1 overflow-y-auto">
{isLoading ? (
<div className="p-4 text-center text-[var(--muted-foreground)]">
Chargement des notes...
</div>
) : filteredNotes.length === 0 ? (
<div className="p-4 text-center text-[var(--muted-foreground)]">
{searchQuery ? 'Aucune note trouvée' : 'Aucune note pour le moment'}
</div>
) : (
<div className="p-2 space-y-4">
{Object.entries(groupedNotes).map(([groupName, groupNotes]) => (
<div key={groupName}>
{/* Group Header */}
<div className="px-3 py-2 bg-[var(--card)]/30 backdrop-blur-sm border-b border-[var(--border)]/30 rounded-t-lg">
<h3 className="text-xs font-mono font-medium text-[var(--primary)] uppercase tracking-wider">
{groupName} ({groupNotes.length})
</h3>
</div>
{/* Group Notes */}
<div className="space-y-1">
{groupNotes.map((note) => (
<div
key={note.id}
onClick={() => onSelectNote(note)}
className={`group relative p-3 cursor-pointer transition-all duration-200 backdrop-blur-sm ${
selectedNoteId === note.id
? 'bg-[var(--primary)]/20 border border-[var(--primary)]/30 shadow-lg shadow-[var(--primary)]/10'
: 'bg-[var(--card)]/30 hover:bg-[var(--card)]/50 border border-[var(--border)]/40 hover:border-[var(--border)]/60 hover:shadow-md'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-[var(--foreground)] truncate mb-2">
{getNoteTitle(note.content)}
</h3>
{/* Tags - seulement si pas groupé par tags */}
{!groupByTags &&
note.tags &&
note.tags.length > 0 && (
<div className="mb-2">
<TagDisplay
tags={note.tags}
availableTags={availableTags}
maxTags={2}
size="sm"
showColors={true}
showDot={false}
/>
</div>
)}
<div className="flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
<Calendar className="w-3 h-3" />
<span suppressHydrationWarning>
{formatDistanceToNow(new Date(note.updatedAt), {
addSuffix: true,
locale: fr,
})}
</span>
</div>
</div>
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={(e) => handleDeleteClick(note.id, e)}
className="p-1 hover:bg-[var(--destructive)]/10 rounded text-[var(--destructive)] transition-colors"
title="Supprimer"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
{/* Delete Confirmation */}
{showDeleteConfirm === note.id && (
<div className="absolute inset-0 bg-[var(--destructive)]/5 border border-[var(--destructive)]/20 rounded-lg p-3 backdrop-blur-sm">
<div className="text-sm text-[var(--foreground)] mb-2">
Supprimer cette note ?
</div>
<div className="flex gap-2">
<button
onClick={() => handleDeleteConfirm(note.id)}
className="px-2 py-1 bg-[var(--destructive)] text-[var(--primary-foreground)] rounded text-xs hover:bg-[var(--destructive)]/90"
>
Supprimer
</button>
<button
onClick={handleDeleteCancel}
className="px-2 py-1 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-xs hover:bg-[var(--card-hover)]"
>
Annuler
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}