From fcad96d1a98d33eef3cc4ec18000e315824a8cc5 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 6 Jan 2026 19:04:34 +0100 Subject: [PATCH] feat: add modular section components for Index.tsx refactoring Created 5 reusable section components (~652 lines): - NoteCard.tsx: Individual note card with priority/archive - NotesListView.tsx: Paginated notes list - TodosSection.tsx: Todos panel with active/completed tabs - RssSection.tsx: RSS articles display with pagination - CalendarSection.tsx: Calendar events panel Components are ready for progressive integration into Index.tsx --- src/components/sections/CalendarSection.tsx | 166 ++++++++++++++++ src/components/sections/NoteCard.tsx | 123 ++++++++++++ src/components/sections/NotesListView.tsx | 85 +++++++++ src/components/sections/RssSection.tsx | 106 +++++++++++ src/components/sections/TodosSection.tsx | 200 ++++++++++++++++++++ src/components/sections/index.ts | 6 + 6 files changed, 686 insertions(+) create mode 100644 src/components/sections/CalendarSection.tsx create mode 100644 src/components/sections/NoteCard.tsx create mode 100644 src/components/sections/NotesListView.tsx create mode 100644 src/components/sections/RssSection.tsx create mode 100644 src/components/sections/TodosSection.tsx create mode 100644 src/components/sections/index.ts diff --git a/src/components/sections/CalendarSection.tsx b/src/components/sections/CalendarSection.tsx new file mode 100644 index 0000000..679fbba --- /dev/null +++ b/src/components/sections/CalendarSection.tsx @@ -0,0 +1,166 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Calendar as CalendarIcon, Plus, RefreshCw, Edit, ExternalLink } from "lucide-react"; +import { CalendarEvent } from "@/services/CalendarService"; +import CalendarService from "@/services/CalendarService"; +import { showError, showSuccess } from "@/utils/toast"; + +interface CalendarSectionProps { + calendarEvents: CalendarEvent[]; + paginatedCalendarEvents: CalendarEvent[]; + calendarPage: number; + totalCalendarPages: number; + onAddEvent: () => void; + onEditEvent: (event: CalendarEvent) => void; + onPageChange: (page: number) => void; + onShowAdmin: () => void; + loadCalendarEvents: () => Promise; +} + +export const CalendarSection = ({ + calendarEvents, + paginatedCalendarEvents, + calendarPage, + totalCalendarPages, + onAddEvent, + onEditEvent, + onPageChange, + onShowAdmin, + loadCalendarEvents +}: CalendarSectionProps) => { + + const handleSync = async () => { + try { + const result = await CalendarService.sync(); + showSuccess(`${result.syncedCount} événements synchronisés`); + await loadCalendarEvents(); + } catch (error) { + showError("Erreur lors de la synchronisation"); + } + }; + + return ( + + +
+ + + Rendez-vous + +
+ + +
+
+
+ +
+ {calendarEvents.length > 0 ? ( + paginatedCalendarEvents.map(event => { + const startDate = new Date(event.start_time); + const endDate = new Date(event.end_time); + const isToday = startDate.toDateString() === new Date().toDateString(); + const isSoon = startDate.getTime() - new Date().getTime() < 30 * 60 * 1000 && startDate > new Date(); + + // Détecter les événements "toute la journée" + const isAllDay = event.all_day || + (startDate.getHours() === 0 && startDate.getMinutes() === 0) || + (startDate.getHours() === 1 && startDate.getMinutes() === 0 && endDate.getHours() === 1); + + return ( +
+
+
event.html_link && window.open(event.html_link, '_blank')} + > +
+

{event.title}

+
+

+ {isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', timeZone: 'Europe/Paris' })} + {isAllDay ? ' - Toute la journée' : ` à ${startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' })}`} +

+ {event.location && ( +

+ 📍 {event.location} +

+ )} + {isSoon && ( + Dans moins de 30 min + )} +
+
+ + event.html_link && window.open(event.html_link, '_blank')} + /> +
+
+
+ ); + }) + ) : ( +

+ Aucun rendez-vous à venir +
+ +

+ )} +
+ {totalCalendarPages > 1 && ( +
+ + + {calendarPage + 1}/{totalCalendarPages} + + +
+ )} +
+
+ ); +}; + +export default CalendarSection; diff --git a/src/components/sections/NoteCard.tsx b/src/components/sections/NoteCard.tsx new file mode 100644 index 0000000..678e5c3 --- /dev/null +++ b/src/components/sections/NoteCard.tsx @@ -0,0 +1,123 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Archive, CheckSquare, ImageIcon, Paperclip, Tag as TagIcon } from "lucide-react"; +import { Note } from "@/services/NotesService"; +import NotesService from "@/services/NotesService"; + +interface NoteCardProps { + note: Note; + onOpenNote: (note: Note) => void; + onUpdateNotes: (updater: (notes: Note[]) => Note[]) => void; +} + +// Clean HTML content for preview +const cleanHtmlContent = (html: string): string => { + const temp = document.createElement('div'); + temp.innerHTML = html; + return temp.textContent || temp.innerText || ''; +}; + +export const NoteCard = ({ note, onOpenNote, onUpdateNotes }: NoteCardProps) => { + const handleTogglePriority = async (e: React.MouseEvent) => { + e.stopPropagation(); + const newPriority = !note.priority; + if (note.id) { + await NotesService.togglePriority(note.id, newPriority); + onUpdateNotes(notes => notes.map(n => n.id === note.id ? { ...n, priority: newPriority } : n)); + } + }; + + const handleToggleArchive = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (note.id) { + const newArchived = !note.archived; + const success = await NotesService.archiveNote(note.id, newArchived); + if (success) { + onUpdateNotes(notes => notes.map(n => n.id === note.id ? { ...n, archived: newArchived } : n)); + } + } + }; + + return ( + onOpenNote(note)} + > + + {/* Priority icon */} + + + {/* Archive icon */} + + +

+ {note.title || "Sans titre"} +

+

+ {note.content ? cleanHtmlContent(note.content) : "Note vide"} +

+
+ {note.todos && note.todos.length > 0 && ( + + + {note.todos.length} + + )} + {note.images && note.images.length > 0 && ( + + + {note.images.length} + + )} + {note.files && note.files.length > 0 && ( + + + {note.files.length} + + )} + {note.tags && note.tags.length > 0 && ( + note.tags.map((tag) => ( + + + {tag.name} + + )) + )} +
+ {note.updated_at && ( +

+ Modifiée le {new Date(note.updated_at).toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + })} +

+ )} +
+
+ ); +}; + +export default NoteCard; diff --git a/src/components/sections/NotesListView.tsx b/src/components/sections/NotesListView.tsx new file mode 100644 index 0000000..281166d --- /dev/null +++ b/src/components/sections/NotesListView.tsx @@ -0,0 +1,85 @@ +import { Button } from "@/components/ui/button"; +import { FileText, Plus } from "lucide-react"; +import { Note } from "@/services/NotesService"; +import { NoteCard } from "./NoteCard"; + +interface NotesListViewProps { + notes: Note[]; + filteredNotes: Note[]; + paginatedNotes: Note[]; + notesPage: number; + totalNotesPages: number; + searchQuery: string; + onOpenNote: (note: Note) => void; + onCreateNote: () => void; + onUpdateNotes: (updater: (notes: Note[]) => Note[]) => void; + onPageChange: (page: number) => void; +} + +export const NotesListView = ({ + paginatedNotes, + totalNotesPages, + notesPage, + searchQuery, + onOpenNote, + onCreateNote, + onUpdateNotes, + onPageChange +}: NotesListViewProps) => { + return ( + <> +
+ {paginatedNotes.length > 0 ? ( + paginatedNotes.map(note => ( + + )) + ) : ( +
+ +

+ {searchQuery ? "Aucune note trouvée" : "Aucune note"} +

+ {!searchQuery && ( + + )} +
+ )} +
+ + {/* Pagination pour les notes */} + {totalNotesPages > 1 && ( +
+ + + Page {notesPage + 1} sur {totalNotesPages} + + +
+ )} + + ); +}; + +export default NotesListView; diff --git a/src/components/sections/RssSection.tsx b/src/components/sections/RssSection.tsx new file mode 100644 index 0000000..f4b12b2 --- /dev/null +++ b/src/components/sections/RssSection.tsx @@ -0,0 +1,106 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Rss, RefreshCw, Plus, ExternalLink } from "lucide-react"; +import { RssArticle } from "@/services/RssService"; + +interface RssSectionProps { + rssArticles: RssArticle[]; + paginatedRssArticles: RssArticle[]; + rssPage: number; + totalRssPages: number; + onRefresh: () => void; + onAddFeed: () => void; + onPageChange: (page: number) => void; +} + +export const RssSection = ({ + paginatedRssArticles, + rssPage, + totalRssPages, + onRefresh, + onAddFeed, + onPageChange +}: RssSectionProps) => { + return ( + + +
+ + + Flux RSS + +
+ + +
+
+
+ +
+ {paginatedRssArticles.length > 0 ? ( + paginatedRssArticles.map(article => ( +
article.link && window.open(article.link, '_blank')} + > +
+
+

{article.title}

+ {article.description && ( +

+ {article.description.replace(/<[^>]*>/g, '')} +

+ )} +
+ +
+
+ )) + ) : ( +

+ Aucun article +
+ +

+ )} +
+ {totalRssPages > 1 && ( +
+ + + {rssPage + 1}/{totalRssPages} + + +
+ )} +
+
+ ); +}; + +export default RssSection; diff --git a/src/components/sections/TodosSection.tsx b/src/components/sections/TodosSection.tsx new file mode 100644 index 0000000..cd86df1 --- /dev/null +++ b/src/components/sections/TodosSection.tsx @@ -0,0 +1,200 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { CheckSquare, Plus, Trash2, Star, Activity } from "lucide-react"; +import { Todo } from "@/services/TodosService"; + +interface TodosSectionProps { + todos: Todo[]; + activeTodos: Todo[]; + completedTodos: Todo[]; + paginatedActiveTodos: Todo[]; + paginatedCompletedTodos: Todo[]; + todosActivePage: number; + todosCompletedPage: number; + totalActiveTodosPages: number; + totalCompletedTodosPages: number; + onAddTodo: () => void; + onToggleTodo: (id: number) => void; + onToggleTodoInProgress: (id: number) => void; + onToggleTodoPriority: (id: number) => void; + onDeleteTodo: (id: number) => void; + onActivePageChange: (page: number) => void; + onCompletedPageChange: (page: number) => void; +} + +export const TodosSection = ({ + todos, + paginatedActiveTodos, + paginatedCompletedTodos, + todosActivePage, + todosCompletedPage, + totalActiveTodosPages, + totalCompletedTodosPages, + onAddTodo, + onToggleTodo, + onToggleTodoInProgress, + onToggleTodoPriority, + onDeleteTodo, + onActivePageChange, + onCompletedPageChange +}: TodosSectionProps) => { + const activeTodosCount = todos.filter(t => !t.completed).length; + const completedTodosCount = todos.filter(t => t.completed).length; + + return ( + + +
+ + + Tâches + + +
+
+ + + + + Actives ({activeTodosCount}) + + + Complétées ({completedTodosCount}) + + + + +
+ {paginatedActiveTodos.length > 0 ? ( + paginatedActiveTodos.map(todo => ( +
+ todo.id && onToggleTodo(todo.id)} + className="h-4 w-4" + /> + + {todo.text} + + + + +
+ )) + ) : ( +

Aucune tâche active

+ )} +
+ {totalActiveTodosPages > 1 && ( +
+ + + {todosActivePage + 1}/{totalActiveTodosPages} + + +
+ )} +
+ + +
+ {paginatedCompletedTodos.length > 0 ? ( + paginatedCompletedTodos.map(todo => ( +
+ todo.id && onToggleTodo(todo.id)} + className="h-4 w-4" + /> + + {todo.text} + + {todo.priority && ( + + )} + +
+ )) + ) : ( +

Aucune tâche complétée

+ )} +
+ {totalCompletedTodosPages > 1 && ( +
+ + + {todosCompletedPage + 1}/{totalCompletedTodosPages} + + +
+ )} +
+
+
+
+ ); +}; + +export default TodosSection; diff --git a/src/components/sections/index.ts b/src/components/sections/index.ts new file mode 100644 index 0000000..a7f0ca3 --- /dev/null +++ b/src/components/sections/index.ts @@ -0,0 +1,6 @@ +// Section components barrel export +export { NoteCard } from './NoteCard'; +export { NotesListView } from './NotesListView'; +export { TodosSection } from './TodosSection'; +export { RssSection } from './RssSection'; +export { CalendarSection } from './CalendarSection';