diff --git a/src/App.tsx b/src/App.tsx index 9b9a568..42ed1da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,9 +5,17 @@ import { ThemeProvider } from "@/components/theme-provider"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { useEffect, useState } from "react"; +import { useIsMobile } from "@/hooks/use-mobile"; import Index from "./pages/Index"; import Login from "./pages/Login"; import NotFound from "./pages/NotFound"; +import MobileDashboard from "./pages/mobile/MobileDashboard"; +import CalendarPage from "./pages/mobile/CalendarPage"; +import NotesPage from "./pages/mobile/NotesPage"; +import NoteDetailPage from "./pages/mobile/NoteDetailPage"; +import TodosPage from "./pages/mobile/TodosPage"; +import RssPage from "./pages/mobile/RssPage"; +import SettingsPage from "./pages/mobile/SettingsPage"; import AuthService from "./services/AuthService"; import "./globals.css"; @@ -22,6 +30,22 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { return <>{children}; }; +// Auto-redirect component based on device +const AutoRedirect = () => { + const isMobile = useIsMobile(); + + if (isMobile === undefined) { + // Still detecting, show loading + return ( +
+
+
+ ); + } + + return ; +}; + const App = () => { const [isLoading, setIsLoading] = useState(true); @@ -56,14 +80,44 @@ const App = () => { } /> + + {/* Auto-redirect root to mobile or desktop */} + + + } + /> + + {/* Desktop route */} + } /> + + {/* Mobile routes */} + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + + } /> diff --git a/src/components/mobile/MobileCard.tsx b/src/components/mobile/MobileCard.tsx new file mode 100644 index 0000000..ba87b09 --- /dev/null +++ b/src/components/mobile/MobileCard.tsx @@ -0,0 +1,42 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { LucideIcon } from "lucide-react"; + +interface MobileCardProps { + title?: string; + icon?: LucideIcon; + children: React.ReactNode; + className?: string; + headerAction?: React.ReactNode; + onClick?: () => void; +} + +export const MobileCard = ({ + title, + icon: Icon, + children, + className = "", + headerAction, + onClick +}: MobileCardProps) => { + return ( + + {(title || Icon) && ( + +
+ + {Icon && } + {title} + + {headerAction} +
+
+ )} + + {children} + +
+ ); +}; diff --git a/src/components/mobile/MobileFAB.tsx b/src/components/mobile/MobileFAB.tsx new file mode 100644 index 0000000..f6b2dbd --- /dev/null +++ b/src/components/mobile/MobileFAB.tsx @@ -0,0 +1,21 @@ +import { Button } from "@/components/ui/button"; +import { LucideIcon } from "lucide-react"; + +interface MobileFABProps { + icon: LucideIcon; + onClick: () => void; + label?: string; +} + +export const MobileFAB = ({ icon: Icon, onClick, label }: MobileFABProps) => { + return ( + + ); +}; diff --git a/src/components/mobile/MobileHeader.tsx b/src/components/mobile/MobileHeader.tsx new file mode 100644 index 0000000..575e259 --- /dev/null +++ b/src/components/mobile/MobileHeader.tsx @@ -0,0 +1,54 @@ +import { Menu, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +interface MobileHeaderProps { + title: string; + onMenuClick?: () => void; + onBackClick?: () => void; + showBack?: boolean; + rightAction?: React.ReactNode; +} + +export const MobileHeader = ({ + title, + onMenuClick, + onBackClick, + showBack = false, + rightAction +}: MobileHeaderProps) => { + return ( +
+
+
+ {showBack ? ( + + ) : ( + + )} +
+ +

+ {title} +

+ +
+ {rightAction ||
} +
+
+
+ ); +}; diff --git a/src/pages/mobile/CalendarPage.tsx b/src/pages/mobile/CalendarPage.tsx new file mode 100644 index 0000000..ebc3b74 --- /dev/null +++ b/src/pages/mobile/CalendarPage.tsx @@ -0,0 +1,186 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { MobileFAB } from "@/components/mobile/MobileFAB"; +import { MobileCard } from "@/components/mobile/MobileCard"; +import { Plus, RefreshCw, ExternalLink, Edit } from "lucide-react"; +import CalendarService, { CalendarEvent } from "@/services/CalendarService"; +import { showError, showSuccess } from "@/utils/toast"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +const EVENTS_PER_PAGE = 10; + +export default function CalendarPage() { + const [events, setEvents] = useState([]); + const [page, setPage] = useState(0); + const [addEventModal, setAddEventModal] = useState(false); + const [editEventModal, setEditEventModal] = useState<{ open: boolean; event?: CalendarEvent }>({ open: false }); + const [loading, setLoading] = useState(false); + + const loadEvents = async () => { + try { + const data = await CalendarService.getEvents(100); + const sorted = data.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()); + setEvents(sorted); + } catch (error) { + showError("Erreur lors du chargement des événements"); + } + }; + + useEffect(() => { + loadEvents(); + }, []); + + const handleSync = async () => { + setLoading(true); + try { + const result = await CalendarService.sync(); + showSuccess(`${result.syncedCount} événements synchronisés`); + await loadEvents(); + } catch (error) { + showError("Erreur lors de la synchronisation"); + } finally { + setLoading(false); + } + }; + + const totalPages = Math.ceil(events.length / EVENTS_PER_PAGE); + const paginatedEvents = events.slice(page * EVENTS_PER_PAGE, (page + 1) * EVENTS_PER_PAGE); + + return ( +
+ {/* Header Actions */} +
+ +
+ + {/* Events List */} +
+ {events.length > 0 ? ( + paginatedEvents.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(); + 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 + + )} +
+ +
+
+ ); + }) + ) : ( +
+

Aucun rendez-vous à venir

+ +
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + {page + 1} / {totalPages} + + +
+ )} + + {/* FAB */} + setAddEventModal(true)} label="Ajouter un événement" /> + + {/* Add Event Modal - Placeholder for now */} + + + + Nouvel événement + + Cette fonctionnalité sera disponible prochainement + + + + + + {/* Edit Event Modal - Placeholder for now */} + setEditEventModal({ open })}> + + + Modifier l'événement + + Cette fonctionnalité sera disponible prochainement + + + + +
+ ); +} diff --git a/src/pages/mobile/MobileDashboard.tsx b/src/pages/mobile/MobileDashboard.tsx new file mode 100644 index 0000000..2509546 --- /dev/null +++ b/src/pages/mobile/MobileDashboard.tsx @@ -0,0 +1,138 @@ +import { useState } from "react"; +import { Outlet, useNavigate, useLocation } from "react-router-dom"; +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { + Menu, + Calendar, + FileText, + CheckSquare, + Rss, + Settings, + LogOut, + User +} from "lucide-react"; +import AuthService from "@/services/AuthService"; + +const menuItems = [ + { icon: Calendar, label: "Calendrier", path: "/mobile/calendar" }, + { icon: FileText, label: "Notes", path: "/mobile/notes" }, + { icon: CheckSquare, label: "Tâches", path: "/mobile/todos" }, + { icon: Rss, label: "RSS", path: "/mobile/rss" }, + { icon: Settings, label: "Paramètres", path: "/mobile/settings" }, +]; + +export default function MobileDashboard() { + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + const location = useLocation(); + + const handleLogout = () => { + AuthService.logout(); + navigate("/login"); + }; + + const handleMenuClick = (path: string) => { + navigate(path); + setOpen(false); + }; + + const getCurrentTitle = () => { + const item = menuItems.find(item => location.pathname.startsWith(item.path)); + return item?.label || "NoteFlow"; + }; + + return ( +
+ {/* Header */} +
+
+ + + + + +
+ {/* Logo */} +
+

NoteFlow

+
+ + {/* Menu Items */} + + + {/* User Section */} +
+ +
+
+
+
+ +

+ {getCurrentTitle()} +

+ + + + + + + Mon compte + + handleMenuClick("/mobile/settings")}> + + Paramètres + + + + + Déconnexion + + + +
+
+ + {/* Content */} +
+ +
+
+ ); +} diff --git a/src/pages/mobile/NoteDetailPage.tsx b/src/pages/mobile/NoteDetailPage.tsx new file mode 100644 index 0000000..18f88a9 --- /dev/null +++ b/src/pages/mobile/NoteDetailPage.tsx @@ -0,0 +1,326 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Card, CardContent } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { MobileHeader } from "@/components/mobile/MobileHeader"; +import { RichTextEditor } from "@/components/RichTextEditor"; +import { + ArrowLeft, + Archive, + Trash2, + CheckSquare, + Image as ImageIcon, + Paperclip, + Tag as TagIcon, + X, + MoreVertical +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import NotesService, { Note } from "@/services/NotesService"; +import { showError, showSuccess } from "@/utils/toast"; + +export default function NoteDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + const [note, setNote] = useState(null); + const [addTodoModal, setAddTodoModal] = useState(false); + const [todoText, setTodoText] = useState(""); + const [deleteModal, setDeleteModal] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + + useEffect(() => { + loadNote(); + }, [id]); + + const loadNote = async () => { + if (!id) return; + try { + const notes = await NotesService.getNotes(); + const foundNote = notes.find(n => n.id === parseInt(id)); + if (foundNote) { + setNote(foundNote); + } else { + showError("Note introuvable"); + navigate("/mobile/notes"); + } + } catch (error) { + showError("Erreur lors du chargement de la note"); + } + }; + + const updateNote = async (updates: Partial) => { + if (!note) return; + try { + const updated = { ...note, ...updates }; + await NotesService.updateNote(updated); + setNote(updated); + } catch (error) { + showError("Erreur lors de la sauvegarde"); + } + }; + + const handleDelete = async () => { + if (!note?.id) return; + try { + await NotesService.deleteNote(note.id); + showSuccess("Note supprimée"); + navigate("/mobile/notes"); + } catch (error) { + showError("Erreur lors de la suppression"); + } + }; + + const handleAddTodo = async () => { + if (!note || !todoText.trim()) return; + const newTodo = { text: todoText, completed: false }; + const updatedTodos = [...(note.todos || []), newTodo]; + await updateNote({ todos: updatedTodos }); + setTodoText(""); + setAddTodoModal(false); + showSuccess("Tâche ajoutée"); + }; + + const handleToggleTodo = async (index: number) => { + if (!note?.todos) return; + const updatedTodos = [...note.todos]; + updatedTodos[index] = { ...updatedTodos[index], completed: !updatedTodos[index].completed }; + await updateNote({ todos: updatedTodos }); + }; + + const handleDeleteTodo = async (index: number) => { + if (!note?.todos) return; + const updatedTodos = [...note.todos]; + updatedTodos.splice(index, 1); + await updateNote({ todos: updatedTodos }); + showSuccess("Tâche supprimée"); + }; + + const handleImageUpload = async (e: React.ChangeEvent) => { + if (!e.target.files || !e.target.files[0] || !note?.id) return; + try { + const image = await NotesService.uploadImage(note.id, e.target.files[0]); + if (image) { + const updatedImages = [...(note.images || []), image]; + setNote({ ...note, images: updatedImages }); + showSuccess("Image ajoutée"); + } + } catch (error) { + showError("Erreur lors de l'upload"); + } + e.target.value = ''; + }; + + const handleDeleteImage = async (imageId: number, index: number) => { + if (!note?.id) return; + try { + await NotesService.deleteImage(note.id, imageId); + const updatedImages = (note.images || []).filter((_, i) => i !== index); + setNote({ ...note, images: updatedImages }); + showSuccess("Image supprimée"); + } catch (error) { + showError("Erreur lors de la suppression"); + } + }; + + if (!note) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Fixed Header */} +
+
+ + +

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

+ + + + + + + updateNote({ archived: !note.archived })}> + + {note.archived ? "Désarchiver" : "Archiver"} + + setDeleteModal(true)} className="text-red-600"> + + Supprimer + + + +
+
+ + {/* Scrollable Content */} +
+ {/* Title */} + updateNote({ title: e.target.value })} + /> + + {/* Action Buttons */} +
+ + + +
+ + {/* Todos */} + {note.todos && note.todos.length > 0 && ( + + + {note.todos.map((todo, index) => ( +
+ handleToggleTodo(index)} + /> + + {todo.text} + + +
+ ))} +
+
+ )} + + {/* Images */} + {note.images && note.images.length > 0 && ( +
+ {note.images.map((image, index) => ( +
+ {image.original_name} setSelectedImage(`/uploads/images/${image.filename}`)} + /> + +
+ ))} +
+ )} + + {/* Rich Text Editor */} +
+ updateNote({ content })} + /> +
+
+ + {/* Add Todo Modal */} + + + + Ajouter une tâche + + setTodoText(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAddTodo()} + /> + + + + + + + + {/* Delete Confirmation */} + + + + Supprimer la note ? + +

+ Cette action est irréversible. +

+ + + + +
+
+ + {/* Image Preview */} + setSelectedImage(null)}> + + Preview + + +
+ ); +} diff --git a/src/pages/mobile/NotesPage.tsx b/src/pages/mobile/NotesPage.tsx new file mode 100644 index 0000000..d42f085 --- /dev/null +++ b/src/pages/mobile/NotesPage.tsx @@ -0,0 +1,243 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { MobileFAB } from "@/components/mobile/MobileFAB"; +import { MobileCard } from "@/components/mobile/MobileCard"; +import { Plus, Search, CheckSquare, Image as ImageIcon, Paperclip, Tag as TagIcon, FileText } from "lucide-react"; +import NotesService, { Note } from "@/services/NotesService"; +import { showError, showSuccess } from "@/utils/toast"; + +const NOTES_PER_PAGE = 10; + +export default function NotesPage() { + const [notes, setNotes] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [showArchived, setShowArchived] = useState(false); + const [page, setPage] = useState(0); + const navigate = useNavigate(); + + const loadNotes = async () => { + try { + const data = await NotesService.getNotes(); + setNotes(data); + } catch (error) { + showError("Erreur lors du chargement des notes"); + } + }; + + useEffect(() => { + loadNotes(); + }, []); + + 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; + }) + .sort((a, b) => { + if (a.priority && !b.priority) return -1; + if (!a.priority && b.priority) return 1; + return new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime(); + }); + + const totalPages = Math.ceil(filteredNotes.length / NOTES_PER_PAGE); + const paginatedNotes = filteredNotes.slice(page * NOTES_PER_PAGE, (page + 1) * NOTES_PER_PAGE); + + const handleCreateNote = async () => { + try { + const newNote = await NotesService.createNote("Nouvelle note", ""); + navigate(`/mobile/notes/${newNote.id}`); + } catch (error) { + showError("Erreur lors de la création de la note"); + } + }; + + const handleTogglePriority = async (note: Note, e: React.MouseEvent) => { + e.stopPropagation(); + try { + const newPriority = !note.priority; + if (note.id) { + await NotesService.togglePriority(note.id, newPriority); + setNotes(notes.map(n => n.id === note.id ? { ...n, priority: newPriority } : n)); + showSuccess(newPriority ? "Note marquée comme prioritaire" : "Priorité retirée"); + } + } catch (error) { + showError("Erreur lors de la modification"); + } + }; + + return ( +
+ {/* Search Bar */} +
+
+ + { + setSearchQuery(e.target.value); + setPage(0); + }} + /> +
+ + {/* Filter Tabs */} +
+ + +
+
+ + {/* Notes List */} +
+ {paginatedNotes.length > 0 ? ( + paginatedNotes.map(note => ( + navigate(`/mobile/notes/${note.id}`)} + > +
+ {/* Priority Button */} + + +

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

+ +

+ {note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"} +

+ + {/* Badges */} +
+ {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.slice(0, 2).map((tag) => ( + + + {tag.name} + + )) + )} + {note.tags && note.tags.length > 2 && ( + + +{note.tags.length - 2} + + )} +
+ + {/* Date */} + {note.updated_at && ( +

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

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

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

+ {!searchQuery && ( + + )} +
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + {page + 1} / {totalPages} + + +
+ )} + + {/* FAB */} + +
+ ); +} diff --git a/src/pages/mobile/RssPage.tsx b/src/pages/mobile/RssPage.tsx new file mode 100644 index 0000000..640c1cc --- /dev/null +++ b/src/pages/mobile/RssPage.tsx @@ -0,0 +1,189 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { MobileFAB } from "@/components/mobile/MobileFAB"; +import { MobileCard } from "@/components/mobile/MobileCard"; +import { Plus, RefreshCw, ExternalLink, Rss as RssIcon } from "lucide-react"; +import RssService, { RssArticle } from "@/services/RssService"; +import { showError, showSuccess } from "@/utils/toast"; + +const ARTICLES_PER_PAGE = 10; + +export default function RssPage() { + const [articles, setArticles] = useState([]); + const [page, setPage] = useState(0); + const [loading, setLoading] = useState(false); + const [addFeedModal, setAddFeedModal] = useState(false); + const [feedUrl, setFeedUrl] = useState(""); + const [feedTitle, setFeedTitle] = useState(""); + + const loadArticles = async () => { + try { + const data = await RssService.getArticles(100); + setArticles(data); + } catch (error) { + showError("Erreur lors du chargement des articles"); + } + }; + + useEffect(() => { + loadArticles(); + }, []); + + const handleRefresh = async () => { + setLoading(true); + try { + await RssService.refreshFeeds(); + await loadArticles(); + showSuccess("Flux RSS actualisés"); + } catch (error) { + showError("Erreur lors de l'actualisation"); + } finally { + setLoading(false); + } + }; + + const handleAddFeed = async () => { + if (!feedUrl.trim()) { + showError("L'URL est requise"); + return; + } + try { + await RssService.addFeed(feedUrl, feedTitle || "Nouveau flux"); + showSuccess("Flux RSS ajouté"); + setFeedUrl(""); + setFeedTitle(""); + setAddFeedModal(false); + await handleRefresh(); + } catch (error) { + showError("Erreur lors de l'ajout du flux"); + } + }; + + const totalPages = Math.ceil(articles.length / ARTICLES_PER_PAGE); + const paginatedArticles = articles.slice(page * ARTICLES_PER_PAGE, (page + 1) * ARTICLES_PER_PAGE); + + return ( +
+ {/* Header Actions */} +
+ +
+ + {/* Articles List */} +
+ {paginatedArticles.length > 0 ? ( + paginatedArticles.map(article => ( + article.link && window.open(article.link, '_blank')} + > +
+
+

{article.title}

+ {article.description && ( +

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

+ )} + {article.pub_date && ( +

+ {new Date(article.pub_date).toLocaleDateString('fr-FR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + })} +

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

Aucun article

+ +
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + {page + 1} / {totalPages} + + +
+ )} + + {/* FAB */} + setAddFeedModal(true)} label="Ajouter un flux RSS" /> + + {/* Add Feed Modal */} + + + + Ajouter un flux RSS + +
+
+ + setFeedUrl(e.target.value)} + /> +
+
+ + setFeedTitle(e.target.value)} + /> +
+
+ + + + +
+
+
+ ); +} diff --git a/src/pages/mobile/SettingsPage.tsx b/src/pages/mobile/SettingsPage.tsx new file mode 100644 index 0000000..8f528ad --- /dev/null +++ b/src/pages/mobile/SettingsPage.tsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { + User, + LogOut, + Calendar, + Rss, + Moon, + Sun, + Zap, + ChevronRight +} from "lucide-react"; +import { useTheme } from "@/components/theme-provider"; +import AuthService from "@/services/AuthService"; +import CalendarService from "@/services/CalendarService"; +import { showError, showSuccess } from "@/utils/toast"; + +export default function SettingsPage() { + const { theme, setTheme } = useTheme(); + const navigate = useNavigate(); + const [calendarStatus, setCalendarStatus] = useState<{ connected: boolean; email?: string } | null>(null); + + useEffect(() => { + loadCalendarStatus(); + }, []); + + const loadCalendarStatus = async () => { + try { + const status = await CalendarService.getAuthStatus(); + setCalendarStatus(status); + } catch (error) { + console.error("Error loading calendar status:", error); + } + }; + + const handleLogout = () => { + AuthService.logout(); + navigate("/login"); + }; + + const handleConnectCalendar = async () => { + try { + const authUrl = await CalendarService.getAuthUrl(); + if (authUrl) { + window.location.href = authUrl; + } + } catch (error) { + showError("Erreur lors de la connexion à Google Calendar"); + } + }; + + const toggleTheme = () => { + setTheme(theme === "dark" ? "light" : "dark"); + }; + + return ( +
+ {/* Appearance */} + + + + {theme === "dark" ? : } + Apparence + + + +
+ + +
+
+
+ + {/* Google Calendar */} + + + + + Google Calendar + + + + {calendarStatus?.connected ? ( +
+

+ Connecté en tant que: +

+

+ {calendarStatus.email} +

+ +
+ ) : ( +
+

+ Connectez votre compte Google pour synchroniser vos événements. +

+ +
+ )} +
+
+ + {/* RSS Feeds */} + + + + + Flux RSS + + + + + + + + {/* OpenRouter AI */} + + + + + OpenRouter AI + + + +

+ Configuration de l'intégration OpenRouter pour les fonctionnalités IA. +

+ +
+
+ + {/* Account */} + + + + + Compte + + + + + + + + {/* App Info */} +
+

NoteFlow Mobile v1.0

+

© 2024 NoteFlow

+
+
+ ); +} diff --git a/src/pages/mobile/TodosPage.tsx b/src/pages/mobile/TodosPage.tsx new file mode 100644 index 0000000..c4a97ee --- /dev/null +++ b/src/pages/mobile/TodosPage.tsx @@ -0,0 +1,228 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { MobileFAB } from "@/components/mobile/MobileFAB"; +import { MobileCard } from "@/components/mobile/MobileCard"; +import { Plus, Trash2, CheckSquare } from "lucide-react"; +import TodosService, { Todo } from "@/services/TodosService"; +import { showError, showSuccess } from "@/utils/toast"; + +const TODOS_PER_PAGE = 15; + +export default function TodosPage() { + const [todos, setTodos] = useState([]); + const [activePage, setActivePage] = useState(0); + const [completedPage, setCompletedPage] = useState(0); + const [addModal, setAddModal] = useState(false); + const [todoText, setTodoText] = useState(""); + + const loadTodos = async () => { + try { + const data = await TodosService.getTodos(); + setTodos(data); + } catch (error) { + showError("Erreur lors du chargement des tâches"); + } + }; + + useEffect(() => { + loadTodos(); + }, []); + + const handleToggle = async (id: number) => { + try { + await TodosService.toggleComplete(id); + setTodos(todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)); + } catch (error) { + showError("Erreur lors de la modification"); + } + }; + + const handleDelete = async (id: number) => { + try { + await TodosService.deleteTodo(id); + setTodos(todos.filter(t => t.id !== id)); + showSuccess("Tâche supprimée"); + } catch (error) { + showError("Erreur lors de la suppression"); + } + }; + + const handleAdd = async () => { + if (!todoText.trim()) return; + try { + const newTodo = await TodosService.createTodo(todoText, 'medium'); + setTodos([newTodo, ...todos]); + setTodoText(""); + setAddModal(false); + showSuccess("Tâche ajoutée"); + } catch (error) { + showError("Erreur lors de la création"); + } + }; + + const activeTodos = todos.filter(t => !t.completed); + const completedTodos = todos.filter(t => t.completed); + + const totalActivePages = Math.ceil(activeTodos.length / TODOS_PER_PAGE); + const totalCompletedPages = Math.ceil(completedTodos.length / TODOS_PER_PAGE); + + const paginatedActive = activeTodos.slice(activePage * TODOS_PER_PAGE, (activePage + 1) * TODOS_PER_PAGE); + const paginatedCompleted = completedTodos.slice(completedPage * TODOS_PER_PAGE, (completedPage + 1) * TODOS_PER_PAGE); + + return ( +
+ + + + Actives ({activeTodos.length}) + + + Complétées ({completedTodos.length}) + + + + {/* Active Todos */} + + {paginatedActive.length > 0 ? ( + paginatedActive.map(todo => ( + +
+ todo.id && handleToggle(todo.id)} + /> + {todo.text} + +
+
+ )) + ) : ( +
+ +

Aucune tâche active

+ +
+ )} + + {totalActivePages > 1 && ( +
+ + + {activePage + 1} / {totalActivePages} + + +
+ )} +
+ + {/* Completed Todos */} + + {paginatedCompleted.length > 0 ? ( + paginatedCompleted.map(todo => ( + +
+ todo.id && handleToggle(todo.id)} + /> + + {todo.text} + + +
+
+ )) + ) : ( +
+

Aucune tâche complétée

+
+ )} + + {totalCompletedPages > 1 && ( +
+ + + {completedPage + 1} / {totalCompletedPages} + + +
+ )} +
+
+ + {/* FAB */} + setAddModal(true)} label="Ajouter une tâche" /> + + {/* Add Modal */} + + + + Nouvelle tâche + + setTodoText(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAdd()} + autoFocus + /> + + + + + + +
+ ); +}