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 */}