feat: Implement mobile notes page with search, advanced filters, and view modes.

This commit is contained in:
Michael committed 2025-12-03 20:48:00 +01:00
1 parent 03986c2547
commit 454fec3382
1 file changed
+113 -135
+113 -135
View File
@@ -8,8 +8,6 @@ import { MobileCard } from "@/components/mobile/MobileCard";
import { Plus, Search, CheckSquare, Image as ImageIcon, Paperclip, Tag as TagIcon, FileText, LayoutGrid, List } from "lucide-react"; import { Plus, Search, CheckSquare, Image as ImageIcon, Paperclip, Tag as TagIcon, FileText, LayoutGrid, List } from "lucide-react";
import NotesService, { Note } from "@/services/NotesService"; import NotesService, { Note } from "@/services/NotesService";
import { showError, showSuccess } from "@/utils/toast"; 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"; import { AdvancedSearch, type SearchFilters, type TagOption } from "@/components/AdvancedSearch";
import { KanbanBoard } from "@/components/KanbanBoard"; import { KanbanBoard } from "@/components/KanbanBoard";
import TagsService from "@/services/TagsService"; import TagsService from "@/services/TagsService";
@@ -30,7 +28,6 @@ export default function NotesPage() {
}); });
const [showArchived, setShowArchived] = useState(false); const [showArchived, setShowArchived] = useState(false);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [templateSelectorOpen, setTemplateSelectorOpen] = useState(false);
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
const navigate = useNavigate(); const navigate = useNavigate();
@@ -122,22 +119,11 @@ export default function NotesPage() {
const totalPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE); const totalPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE);
const paginatedNotes = filteredNotes.slice(page * NOTES_PER_PAGE, (page + 1) * NOTES_PER_PAGE); const paginatedNotes = filteredNotes.slice(page * NOTES_PER_PAGE, (page + 1) * NOTES_PER_PAGE);
const handleCreateNote = () => { const handleCreateNote = async () => {
setTemplateSelectorOpen(true);
};
const handleCreateNoteFromTemplate = async (template: NoteTemplate) => {
try { try {
const title = template.name === "Note vide" ? "Nouvelle note" : template.name; const newNote = await NotesService.createNote("Nouvelle note", "");
const newNote = await NotesService.createNote(title, template.content);
if (newNote) { if (newNote) {
// Add todos if template has any showSuccess("Note créée");
if (template.todos && template.todos.length > 0) {
for (const todo of template.todos) {
await NotesService.addTodo(newNote.id!, todo.text);
}
}
showSuccess("Note créée depuis le template");
navigate(`/mobile/notes/${newNote.id}`); navigate(`/mobile/notes/${newNote.id}`);
} }
} catch (error) { } catch (error) {
@@ -238,124 +224,123 @@ export default function NotesPage() {
{/* Notes View - List or Kanban */} {/* Notes View - List or Kanban */}
{viewMode === 'list' ? ( {viewMode === 'list' ? (
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{paginatedNotes.length > 0 ? ( {paginatedNotes.length > 0 ? (
paginatedNotes.map(note => ( paginatedNotes.map(note => (
<MobileCard <MobileCard
key={note.id} key={note.id}
onClick={() => navigate(`/mobile/notes/${note.id}`)} onClick={() => navigate(`/mobile/notes/${note.id}`)}
> >
<div className="relative"> <div className="relative">
{/* Priority Button */} {/* Priority Button */}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className={`absolute -top-1 -right-1 h-8 w-8 ${ className={`absolute -top-1 -right-1 h-8 w-8 ${note.priority ? 'opacity-100' : 'opacity-50'
note.priority ? 'opacity-100' : 'opacity-50' }`}
}`} onClick={(e) => handleTogglePriority(note, e)}
onClick={(e) => handleTogglePriority(note, e)} >
> <span className={`text-lg font-bold ${note.priority ? 'text-red-500' : 'text-muted-foreground'}`}>
<span className={`text-lg font-bold ${note.priority ? 'text-red-500' : 'text-muted-foreground'}`}> !
! </span>
</span> </Button>
</Button>
<h3 className="font-semibold mb-2 line-clamp-1 pr-8"> <h3 className="font-semibold mb-2 line-clamp-1 pr-8">
{note.title || "Sans titre"} {note.title || "Sans titre"}
</h3> </h3>
<p className="text-sm text-muted-foreground line-clamp-2 mb-3"> <p className="text-sm text-muted-foreground line-clamp-2 mb-3">
{note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"} {note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"}
</p> </p>
{/* Badges */} {/* Badges */}
<div className="flex gap-2 flex-wrap mb-2"> <div className="flex gap-2 flex-wrap mb-2">
{note.todos && note.todos.length > 0 && ( {note.todos && note.todos.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
<CheckSquare className="h-3 w-3 mr-1" /> <CheckSquare className="h-3 w-3 mr-1" />
{note.todos.length} {note.todos.length}
</Badge>
)}
{note.images && note.images.length > 0 && (
<Badge variant="secondary" className="text-xs">
<ImageIcon className="h-3 w-3 mr-1" />
{note.images.length}
</Badge>
)}
{note.files && note.files.length > 0 && (
<Badge variant="secondary" className="text-xs">
<Paperclip className="h-3 w-3 mr-1" />
{note.files.length}
</Badge>
)}
{note.tags && note.tags.length > 0 && (
note.tags.slice(0, 2).map((tag) => (
<Badge key={tag.id} variant="outline" className="text-xs">
<TagIcon className="h-3 w-3 mr-1" />
{tag.name}
</Badge> </Badge>
)) )}
)} {note.images && note.images.length > 0 && (
{note.tags && note.tags.length > 2 && ( <Badge variant="secondary" className="text-xs">
<Badge variant="outline" className="text-xs"> <ImageIcon className="h-3 w-3 mr-1" />
+{note.tags.length - 2} {note.images.length}
</Badge> </Badge>
)}
{note.files && note.files.length > 0 && (
<Badge variant="secondary" className="text-xs">
<Paperclip className="h-3 w-3 mr-1" />
{note.files.length}
</Badge>
)}
{note.tags && note.tags.length > 0 && (
note.tags.slice(0, 2).map((tag) => (
<Badge key={tag.id} variant="outline" className="text-xs">
<TagIcon className="h-3 w-3 mr-1" />
{tag.name}
</Badge>
))
)}
{note.tags && note.tags.length > 2 && (
<Badge variant="outline" className="text-xs">
+{note.tags.length - 2}
</Badge>
)}
</div>
{/* Date */}
{note.updated_at && (
<p className="text-xs text-muted-foreground">
{new Date(note.updated_at).toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})}
</p>
)} )}
</div> </div>
</MobileCard>
))
) : (
<div className="text-center py-16">
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">
{searchQuery ? "Aucune note trouvée" : "Aucune note"}
</p>
{!searchQuery && (
<Button onClick={handleCreateNote} size="lg">
<Plus className="h-5 w-5 mr-2" />
Créer votre première note
</Button>
)}
</div>
)}
{/* Date */} {/* Pagination */}
{note.updated_at && ( {totalPages > 1 && (
<p className="text-xs text-muted-foreground"> <div className="flex items-center justify-center gap-4 px-4 pb-4">
{new Date(note.updated_at).toLocaleDateString('fr-FR', { <Button
day: '2-digit', variant="ghost"
month: '2-digit', size="sm"
year: 'numeric' onClick={() => setPage(p => Math.max(0, p - 1))}
})} disabled={page === 0}
</p> >
)} ‹ Précédent
</div>
</MobileCard>
))
) : (
<div className="text-center py-16">
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">
{searchQuery ? "Aucune note trouvée" : "Aucune note"}
</p>
{!searchQuery && (
<Button onClick={handleCreateNote} size="lg">
<Plus className="h-5 w-5 mr-2" />
Créer votre première note
</Button> </Button>
)} <span className="text-sm text-muted-foreground">
</div> {page + 1} / {totalPages}
)} </span>
<Button
{/* Pagination */} variant="ghost"
{totalPages > 1 && ( size="sm"
<div className="flex items-center justify-center gap-4 px-4 pb-4"> onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
<Button disabled={page === totalPages - 1}
variant="ghost" >
size="sm" Suivant ›
onClick={() => setPage(p => Math.max(0, p - 1))} </Button>
disabled={page === 0} </div>
> )}
‹ Précédent </div>
</Button>
<span className="text-sm text-muted-foreground">
{page + 1} / {totalPages}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
disabled={page === totalPages - 1}
>
Suivant ›
</Button>
</div>
)}
</div>
) : ( ) : (
/* Kanban View */ /* Kanban View */
<div className="p-2 overflow-x-auto"> <div className="p-2 overflow-x-auto">
@@ -363,7 +348,7 @@ export default function NotesPage() {
notes={filteredNotes} notes={filteredNotes}
onNoteClick={(note) => navigate(`/mobile/notes/${note.id}`)} onNoteClick={(note) => navigate(`/mobile/notes/${note.id}`)}
onCreateNote={(columnId) => { onCreateNote={(columnId) => {
setTemplateSelectorOpen(true); handleCreateNote();
}} }}
onMoveNote={async (noteId, newStatus) => { onMoveNote={async (noteId, newStatus) => {
const note = notes.find(n => n.id === noteId); const note = notes.find(n => n.id === noteId);
@@ -393,13 +378,6 @@ export default function NotesPage() {
{/* FAB */} {/* FAB */}
<MobileFAB icon={Plus} onClick={handleCreateNote} label="Créer une note" /> <MobileFAB icon={Plus} onClick={handleCreateNote} label="Créer une note" />
{/* Template Selector */}
<TemplateSelector
open={templateSelectorOpen}
onOpenChange={setTemplateSelectorOpen}
onSelectTemplate={handleCreateNoteFromTemplate}
/>
</div> </div>
); );
} }