mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 07:38:17 +02:00
feat: Implement mobile notes page with search, advanced filters, and view modes.
This commit is contained in:
1 parent
03986c2547
commit
454fec3382
1 file changed
+113
-135
+113
-135
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user