diff --git a/src/components/AdvancedSearch.tsx b/src/components/AdvancedSearch.tsx new file mode 100644 index 0000000..34b365a --- /dev/null +++ b/src/components/AdvancedSearch.tsx @@ -0,0 +1,283 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { Calendar } from "@/components/ui/calendar"; +import { SlidersHorizontal, X, Calendar as CalendarIcon, Tag as TagIcon } from "lucide-react"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; + +export interface SearchFilters { + tags: string[]; // Tag names + dateFrom?: Date; + dateTo?: Date; + hasTodos?: boolean; + hasImages?: boolean; + hasFiles?: boolean; + priority?: boolean; +} + +export interface TagOption { + id: number; + name: string; +} + +interface AdvancedSearchProps { + filters: SearchFilters; + onFiltersChange: (filters: SearchFilters) => void; + availableTags: TagOption[]; +} + +export function AdvancedSearch({ filters, onFiltersChange, availableTags }: AdvancedSearchProps) { + const [open, setOpen] = useState(false); + const [datePickerOpen, setDatePickerOpen] = useState<'from' | 'to' | null>(null); + const [tagSelectorOpen, setTagSelectorOpen] = useState(false); + + const hasActiveFilters = filters.tags.length > 0 || + filters.dateFrom !== undefined || + filters.dateTo !== undefined || + filters.hasTodos !== undefined || + filters.hasImages !== undefined || + filters.hasFiles !== undefined || + filters.priority !== undefined; + + const clearFilters = () => { + onFiltersChange({ + tags: [], + dateFrom: undefined, + dateTo: undefined, + hasTodos: undefined, + hasImages: undefined, + hasFiles: undefined, + priority: undefined + }); + }; + + const toggleTag = (tagName: string) => { + const newTags = filters.tags.includes(tagName) + ? filters.tags.filter(name => name !== tagName) + : [...filters.tags, tagName]; + onFiltersChange({ ...filters, tags: newTags }); + }; + + const selectedTags = availableTags.filter(tag => filters.tags.includes(tag.name)); + + return ( +
+ + + + + +
+
+

Filtres avancés

+ {hasActiveFilters && ( + + )} +
+ + {/* Tags Filter */} +
+ + + + + + + + + + Aucun tag trouvé + + {availableTags.map((tag) => ( + toggleTag(tag.name)} + className="cursor-pointer" + > + + + {tag.name} + + ))} + + + + + + {selectedTags.length > 0 && ( +
+ {selectedTags.map((tag) => ( + + {tag.name} + toggleTag(tag.name)} + /> + + ))} +
+ )} +
+ + {/* Date Range Filter */} +
+ +
+ setDatePickerOpen(open ? 'from' : null)}> + + + + + { + onFiltersChange({ ...filters, dateFrom: date }); + setDatePickerOpen(null); + }} + initialFocus + /> + + + setDatePickerOpen(open ? 'to' : null)}> + + + + + { + onFiltersChange({ ...filters, dateTo: date }); + setDatePickerOpen(null); + }} + initialFocus + /> + + +
+ {(filters.dateFrom || filters.dateTo) && ( + + )} +
+ + {/* Content Type Filters */} +
+ +
+
+ + onFiltersChange({ ...filters, hasTodos: checked ? true : undefined }) + } + /> + +
+
+ + onFiltersChange({ ...filters, hasImages: checked ? true : undefined }) + } + /> + +
+
+ + onFiltersChange({ ...filters, hasFiles: checked ? true : undefined }) + } + /> + +
+
+
+ + {/* Priority Filter */} +
+
+ + onFiltersChange({ ...filters, priority: checked ? true : undefined }) + } + /> + +
+
+
+
+
+ + {hasActiveFilters && ( + + )} +
+ ); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 8103690..33f4907 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -36,6 +36,7 @@ import { ModeToggle } from "@/components/mode-toggle"; import { downloadNoteAsMarkdown, downloadAllNotesAsMarkdown, downloadTodosAsMarkdown } from "@/utils/markdownExport"; import { TemplateSelector } from "@/components/TemplateSelector"; import type { NoteTemplate } from "@/utils/noteTemplates"; +import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch"; // ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS ===== @@ -115,6 +116,15 @@ const Index = () => { const [user, setUser] = useState(null); const [showAdmin, setShowAdmin] = useState(false); const [searchQuery, setSearchQuery] = useState(""); + const [searchFilters, setSearchFilters] = useState({ + tags: [], + dateFrom: undefined, + dateTo: undefined, + hasTodos: undefined, + hasImages: undefined, + hasFiles: undefined, + priority: undefined + }); const [notes, setNotes] = useState([]); const [openNote, setOpenNote] = useState(null); const [users, setUsers] = useState([]); @@ -828,12 +838,71 @@ const Index = () => { } }; - const filteredNotes = notes.filter(note => - (showArchived ? note.archived : !note.archived) && - (searchQuery === "" || - (note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) || - (note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase()))) - ); + // Extract unique tags from all notes + const availableTags: TagOption[] = Array.from( + new Map( + notes.flatMap(note => + (note.tags || []).map(tag => [tag.name, { id: tag.id!, name: tag.name }]) + ) + ).values() + ).sort((a, b) => a.name.localeCompare(b.name)); + + const filteredNotes = notes.filter(note => { + // Archived filter + if (showArchived ? !note.archived : note.archived) return false; + + // Text search + if (searchQuery !== "" && + !(note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) && + !(note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase()))) { + return false; + } + + // Tags filter + if (searchFilters.tags.length > 0) { + const noteTags = (note.tags || []).map(t => t.name); + if (!searchFilters.tags.some(filterTag => noteTags.includes(filterTag))) { + return false; + } + } + + // Date range filter + if (searchFilters.dateFrom || searchFilters.dateTo) { + const noteDate = new Date(note.updated_at || note.created_at || 0); + if (searchFilters.dateFrom) { + const fromDate = new Date(searchFilters.dateFrom); + fromDate.setHours(0, 0, 0, 0); + if (noteDate < fromDate) return false; + } + if (searchFilters.dateTo) { + const toDate = new Date(searchFilters.dateTo); + toDate.setHours(23, 59, 59, 999); + if (noteDate > toDate) return false; + } + } + + // Has todos filter + if (searchFilters.hasTodos && (!note.todos || note.todos.length === 0)) { + return false; + } + + // Has images filter + if (searchFilters.hasImages && (!note.images || note.images.length === 0)) { + return false; + } + + // Has files filter + if (searchFilters.hasFiles && (!note.files || note.files.length === 0)) { + return false; + } + + // Priority filter + if (searchFilters.priority && !note.priority) { + return false; + } + + return true; + }); // Pagination for notes const totalNotesPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE); @@ -845,7 +914,7 @@ const Index = () => { // Reset page when changing filters or search useEffect(() => { setNotesPage(0); - }, [showArchived, searchQuery]); + }, [showArchived, searchQuery, searchFilters]); useEffect(() => { setTodosActivePage(0); @@ -1143,14 +1212,21 @@ const Index = () => { -
- - setSearchQuery(e.target.value)} +
+
+ + setSearchQuery(e.target.value)} + /> +
+
diff --git a/src/pages/mobile/NotesPage.tsx b/src/pages/mobile/NotesPage.tsx index be1a682..83158a1 100644 --- a/src/pages/mobile/NotesPage.tsx +++ b/src/pages/mobile/NotesPage.tsx @@ -10,12 +10,22 @@ import NotesService, { Note } from "@/services/NotesService"; import { showError, showSuccess } from "@/utils/toast"; import { TemplateSelector } from "@/components/TemplateSelector"; import type { NoteTemplate } from "@/utils/noteTemplates"; +import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch"; const NOTES_PER_PAGE = 10; export default function NotesPage() { const [notes, setNotes] = useState([]); const [searchQuery, setSearchQuery] = useState(""); + const [searchFilters, setSearchFilters] = useState({ + tags: [], + dateFrom: undefined, + dateTo: undefined, + hasTodos: undefined, + hasImages: undefined, + hasFiles: undefined, + priority: undefined + }); const [showArchived, setShowArchived] = useState(false); const [page, setPage] = useState(0); const [templateSelectorOpen, setTemplateSelectorOpen] = useState(false); @@ -34,13 +44,71 @@ export default function NotesPage() { loadNotes(); }, []); + // Extract unique tags from all notes + const availableTags: TagOption[] = Array.from( + new Map( + notes.flatMap(note => + (note.tags || []).map(tag => [tag.name, { id: tag.id!, name: tag.name }]) + ) + ).values() + ).sort((a, b) => a.name.localeCompare(b.name)); + const filteredNotes = notes .filter(note => { - const matchArchived = showArchived ? note.archived : !note.archived; - const matchSearch = searchQuery === "" || - note.title?.toLowerCase().includes(searchQuery.toLowerCase()) || - note.content?.toLowerCase().includes(searchQuery.toLowerCase()); - return matchArchived && matchSearch; + // Archived filter + if (showArchived ? !note.archived : note.archived) return false; + + // Text search + if (searchQuery !== "" && + !(note.title?.toLowerCase().includes(searchQuery.toLowerCase())) && + !(note.content?.toLowerCase().includes(searchQuery.toLowerCase()))) { + return false; + } + + // Tags filter + if (searchFilters.tags.length > 0) { + const noteTags = (note.tags || []).map(t => t.name); + if (!searchFilters.tags.some(filterTag => noteTags.includes(filterTag))) { + return false; + } + } + + // Date range filter + if (searchFilters.dateFrom || searchFilters.dateTo) { + const noteDate = new Date(note.updated_at || note.created_at || 0); + if (searchFilters.dateFrom) { + const fromDate = new Date(searchFilters.dateFrom); + fromDate.setHours(0, 0, 0, 0); + if (noteDate < fromDate) return false; + } + if (searchFilters.dateTo) { + const toDate = new Date(searchFilters.dateTo); + toDate.setHours(23, 59, 59, 999); + if (noteDate > toDate) return false; + } + } + + // Has todos filter + if (searchFilters.hasTodos && (!note.todos || note.todos.length === 0)) { + return false; + } + + // Has images filter + if (searchFilters.hasImages && (!note.images || note.images.length === 0)) { + return false; + } + + // Has files filter + if (searchFilters.hasFiles && (!note.files || note.files.length === 0)) { + return false; + } + + // Priority filter + if (searchFilters.priority && !note.priority) { + return false; + } + + return true; }) .sort((a, b) => { if (a.priority && !b.priority) return -1; @@ -106,6 +174,16 @@ export default function NotesPage() { /> + {/* Advanced Filters */} + { + setSearchFilters(newFilters); + setPage(0); + }} + availableTags={availableTags} + /> + {/* Filter Tabs */}