mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 07:38:17 +02:00
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
This commit is contained in:
1 parent
b62e298c8c
commit
fcad96d1a9
6 files changed
+686
No files matched your search
@@ -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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-6 w-6" />
|
||||||
|
Rendez-vous
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<Button size="sm" variant="outline" onClick={onAddEvent}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={handleSync}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3 max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className={`p-3 border rounded-lg hover:bg-accent/50 transition-colors group relative ${isSoon ? 'border-red-500 bg-red-50 dark:bg-red-900/20' :
|
||||||
|
isToday ? 'bg-red-50/50 dark:bg-red-900/10' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div
|
||||||
|
className="flex-1 min-w-0 cursor-pointer"
|
||||||
|
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium line-clamp-2 text-sm">{event.title}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{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' })}`}
|
||||||
|
</p>
|
||||||
|
{event.location && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 truncate">
|
||||||
|
📍 {event.location}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{isSoon && (
|
||||||
|
<Badge variant="destructive" className="mt-1 text-xs">Dans moins de 30 min</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 items-start">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEditEvent(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<ExternalLink
|
||||||
|
className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1 cursor-pointer"
|
||||||
|
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-8 text-sm">
|
||||||
|
Aucun rendez-vous à venir
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="mt-2 text-xs"
|
||||||
|
onClick={onShowAdmin}
|
||||||
|
>
|
||||||
|
Configurer Google Calendar
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{totalCalendarPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-3 pt-3 border-t">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.max(0, calendarPage - 1))}
|
||||||
|
disabled={calendarPage === 0}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{calendarPage + 1}/{totalCalendarPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.min(totalCalendarPages - 1, calendarPage + 1))}
|
||||||
|
disabled={calendarPage === totalCalendarPages - 1}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CalendarSection;
|
||||||
@@ -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 (
|
||||||
|
<Card
|
||||||
|
className="cursor-pointer hover:shadow-md transition-all group relative"
|
||||||
|
onClick={() => onOpenNote(note)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
{/* Priority icon */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`absolute top-2 right-2 h-6 w-6 transition-opacity ${note.priority ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={handleTogglePriority}
|
||||||
|
>
|
||||||
|
{note.priority ? (
|
||||||
|
<span className="text-red-500 text-lg font-bold">!</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-lg">!</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Archive icon */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`absolute top-2 right-10 h-6 w-6 transition-opacity ${note.archived ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={handleToggleArchive}
|
||||||
|
title={note.archived ? "Désarchiver" : "Archiver"}
|
||||||
|
>
|
||||||
|
<Archive className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<h3 className="font-semibold text-base mb-2 line-clamp-1 pr-8">
|
||||||
|
{note.title || "Sans titre"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
||||||
|
{note.content ? cleanHtmlContent(note.content) : "Note vide"}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 flex-wrap mb-2">
|
||||||
|
{note.todos && note.todos.length > 0 && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<CheckSquare className="h-3 w-3 mr-1" />
|
||||||
|
{note.todos.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.images && note.images.length > 0 && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<ImageIcon className="h-3 w-3 mr-1" />
|
||||||
|
{note.images.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.files && note.files.length > 0 && (
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<Paperclip className="h-3 w-3 mr-1" />
|
||||||
|
{note.files.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.tags && note.tags.length > 0 && (
|
||||||
|
note.tags.map((tag) => (
|
||||||
|
<Badge key={tag.id} variant="outline" className="text-xs">
|
||||||
|
<TagIcon className="h-3 w-3 mr-1" />
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{note.updated_at && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Modifiée le {new Date(note.updated_at).toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoteCard;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
{paginatedNotes.length > 0 ? (
|
||||||
|
paginatedNotes.map(note => (
|
||||||
|
<NoteCard
|
||||||
|
key={note.id}
|
||||||
|
note={note}
|
||||||
|
onOpenNote={onOpenNote}
|
||||||
|
onUpdateNotes={onUpdateNotes}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="col-span-1 text-center py-16">
|
||||||
|
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||||
|
<p className="text-muted-foreground text-lg mb-4">
|
||||||
|
{searchQuery ? "Aucune note trouvée" : "Aucune note"}
|
||||||
|
</p>
|
||||||
|
{!searchQuery && (
|
||||||
|
<Button onClick={onCreateNote} size="lg">
|
||||||
|
<Plus className="h-5 w-5 mr-2" />
|
||||||
|
Créer votre première note
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination pour les notes */}
|
||||||
|
{totalNotesPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.max(0, notesPage - 1))}
|
||||||
|
disabled={notesPage === 0}
|
||||||
|
>
|
||||||
|
Précédent
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Page {notesPage + 1} sur {totalNotesPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.min(totalNotesPages - 1, notesPage + 1))}
|
||||||
|
disabled={notesPage >= totalNotesPages - 1}
|
||||||
|
>
|
||||||
|
Suivant
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotesListView;
|
||||||
@@ -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 (
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2 flex-shrink-0">
|
||||||
|
<Rss className="h-5 w-5" />
|
||||||
|
Flux RSS
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex gap-1.5 flex-shrink-0">
|
||||||
|
<Button size="sm" variant="outline" onClick={onRefresh}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={onAddFeed}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-[calc(100vh-500px)] overflow-y-auto">
|
||||||
|
{paginatedRssArticles.length > 0 ? (
|
||||||
|
paginatedRssArticles.map(article => (
|
||||||
|
<div
|
||||||
|
key={article.id}
|
||||||
|
className="p-2.5 border rounded hover:bg-accent/50 transition-colors cursor-pointer"
|
||||||
|
onClick={() => article.link && window.open(article.link, '_blank')}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="font-medium text-sm line-clamp-2 mb-1">{article.title}</h4>
|
||||||
|
{article.description && (
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||||
|
{article.description.replace(/<[^>]*>/g, '')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0 mt-0.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground text-sm py-6">
|
||||||
|
Aucun article
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="mt-2 text-xs"
|
||||||
|
onClick={onAddFeed}
|
||||||
|
>
|
||||||
|
Ajouter un flux RSS
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{totalRssPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-3 pt-3 border-t">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.max(0, rssPage - 1))}
|
||||||
|
disabled={rssPage === 0}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{rssPage + 1}/{totalRssPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(Math.min(totalRssPages - 1, rssPage + 1))}
|
||||||
|
disabled={rssPage >= totalRssPages - 1}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RssSection;
|
||||||
@@ -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 (
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
|
<CheckSquare className="h-5 w-5" />
|
||||||
|
Tâches
|
||||||
|
</CardTitle>
|
||||||
|
<Button size="sm" variant="outline" onClick={onAddTodo}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Tabs defaultValue="active" className="w-full">
|
||||||
|
<TabsList className="grid grid-cols-2 w-full mb-3">
|
||||||
|
<TabsTrigger value="active" className="text-xs">
|
||||||
|
Actives ({activeTodosCount})
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="completed" className="text-xs">
|
||||||
|
Complétées ({completedTodosCount})
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="active" className="mt-0">
|
||||||
|
<div className="space-y-1.5 max-h-[calc(100vh-400px)] overflow-y-auto">
|
||||||
|
{paginatedActiveTodos.length > 0 ? (
|
||||||
|
paginatedActiveTodos.map(todo => (
|
||||||
|
<div key={todo.id} className="flex items-center gap-2 p-2 border rounded hover:bg-accent/50 transition-colors group">
|
||||||
|
<Checkbox
|
||||||
|
checked={false}
|
||||||
|
onCheckedChange={() => todo.id && onToggleTodo(todo.id)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span className="text-sm flex-1 leading-snug">
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`h-7 w-7 transition-all ${todo.in_progress ? 'text-red-500 opacity-100' : 'text-muted-foreground opacity-0 group-hover:opacity-100'}`}
|
||||||
|
onClick={() => todo.id && onToggleTodoInProgress(todo.id)}
|
||||||
|
title={todo.in_progress ? "Marquer comme non commencé" : "Marquer comme en cours"}
|
||||||
|
>
|
||||||
|
<Activity className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`h-7 w-7 ${todo.priority ? '' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}
|
||||||
|
onClick={() => todo.id && onToggleTodoPriority(todo.id)}
|
||||||
|
title={todo.priority ? "Retirer la priorité" : "Marquer comme prioritaire"}
|
||||||
|
>
|
||||||
|
<Star className={`h-3.5 w-3.5 ${todo.priority ? 'fill-yellow-400 text-yellow-400' : ''}`} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() => todo.id && onDeleteTodo(todo.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground text-sm py-6">Aucune tâche active</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{totalActiveTodosPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-3 pt-3 border-t">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onActivePageChange(Math.max(0, todosActivePage - 1))}
|
||||||
|
disabled={todosActivePage === 0}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{todosActivePage + 1}/{totalActiveTodosPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onActivePageChange(Math.min(totalActiveTodosPages - 1, todosActivePage + 1))}
|
||||||
|
disabled={todosActivePage >= totalActiveTodosPages - 1}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="completed" className="mt-0">
|
||||||
|
<div className="space-y-1.5 max-h-[calc(100vh-400px)] overflow-y-auto">
|
||||||
|
{paginatedCompletedTodos.length > 0 ? (
|
||||||
|
paginatedCompletedTodos.map(todo => (
|
||||||
|
<div key={todo.id} className="flex items-center gap-2 p-2 border rounded hover:bg-accent/50 transition-colors group">
|
||||||
|
<Checkbox
|
||||||
|
checked={true}
|
||||||
|
onCheckedChange={() => todo.id && onToggleTodo(todo.id)}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span className="text-sm flex-1 line-through text-muted-foreground leading-snug">
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
{todo.priority && (
|
||||||
|
<Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400 opacity-50" />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() => todo.id && onDeleteTodo(todo.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground text-sm py-6">Aucune tâche complétée</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{totalCompletedTodosPages > 1 && (
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-3 pt-3 border-t">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onCompletedPageChange(Math.max(0, todosCompletedPage - 1))}
|
||||||
|
disabled={todosCompletedPage === 0}
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{todosCompletedPage + 1}/{totalCompletedTodosPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onCompletedPageChange(Math.min(totalCompletedTodosPages - 1, todosCompletedPage + 1))}
|
||||||
|
disabled={todosCompletedPage >= totalCompletedTodosPages - 1}
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TodosSection;
|
||||||
@@ -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';
|
||||||
Reference in new issue
Block a user