diff --git a/routes/rss.routes.js b/routes/rss.routes.js index 6504dad..92865f2 100644 --- a/routes/rss.routes.js +++ b/routes/rss.routes.js @@ -130,6 +130,26 @@ router.post('/fetch', requireAdmin, async (req, res) => { } }); +/** + * POST /api/rss/refresh + * Alias pour fetch (pour compatibilité) + */ +router.post('/refresh', requireAdmin, async (req, res) => { + try { + const rssScheduler = require('../services/rss-scheduler'); + await rssScheduler.manualFetch(); + + // Invalider le cache + articlesCache = null; + articlesCacheTime = 0; + + res.json({ message: 'Mise à jour des flux RSS terminée avec succès' }); + } catch (error) { + logger.error('Erreur lors du refresh des flux RSS:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + // Cache pour les articles RSS (30 secondes) let articlesCache = null; let articlesCacheTime = 0; diff --git a/src/components/modals/AddUserModal.tsx b/src/components/modals/AddUserModal.tsx new file mode 100644 index 0000000..f83f945 --- /dev/null +++ b/src/components/modals/AddUserModal.tsx @@ -0,0 +1,95 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { useState, useEffect } from "react"; +import { UserPlus } from "lucide-react"; + +interface AddUserModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (username: string, password: string, isAdmin: boolean) => void; +} + +export function AddUserModal({ open, onOpenChange, onConfirm }: AddUserModalProps) { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isAdmin, setIsAdmin] = useState(false); + + useEffect(() => { + if (open) { + setUsername(""); + setPassword(""); + setIsAdmin(false); + } + }, [open]); + + const handleConfirm = () => { + if (username.trim() && password.trim()) { + onConfirm(username.trim(), password.trim(), isAdmin); + onOpenChange(false); + } + }; + + const isValid = username.trim().length > 0 && password.trim().length > 0; + + return ( + + + + + + Ajouter un utilisateur + + + Créez un nouveau compte utilisateur pour l'application + + + + + Nom d'utilisateur + setUsername(e.target.value)} + autoFocus + /> + + + Mot de passe + setPassword(e.target.value)} + /> + + + setIsAdmin(checked as boolean)} + /> + + Administrateur + + + + + onOpenChange(false)}> + Annuler + + + Créer l'utilisateur + + + + + ); +} diff --git a/src/components/modals/ConfirmModal.tsx b/src/components/modals/ConfirmModal.tsx new file mode 100644 index 0000000..20af3f8 --- /dev/null +++ b/src/components/modals/ConfirmModal.tsx @@ -0,0 +1,57 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { AlertTriangle } from "lucide-react"; + +interface ConfirmModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + onConfirm: () => void; + confirmText?: string; + cancelText?: string; + variant?: "default" | "destructive"; +} + +export function ConfirmModal({ + open, + onOpenChange, + title, + description, + onConfirm, + confirmText = "Confirmer", + cancelText = "Annuler", + variant = "default" +}: ConfirmModalProps) { + const handleConfirm = () => { + onConfirm(); + onOpenChange(false); + }; + + return ( + + + + + {variant === "destructive" && } + {title} + + + {description} + + + + onOpenChange(false)}> + {cancelText} + + + {confirmText} + + + + + ); +} diff --git a/src/components/modals/InputModal.tsx b/src/components/modals/InputModal.tsx new file mode 100644 index 0000000..2409ad3 --- /dev/null +++ b/src/components/modals/InputModal.tsx @@ -0,0 +1,89 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useState, useEffect } from "react"; + +interface InputModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description?: string; + label: string; + placeholder?: string; + defaultValue?: string; + onConfirm: (value: string) => void; + confirmText?: string; + cancelText?: string; + type?: "text" | "password" | "url" | "email"; +} + +export function InputModal({ + open, + onOpenChange, + title, + description, + label, + placeholder, + defaultValue = "", + onConfirm, + confirmText = "Confirmer", + cancelText = "Annuler", + type = "text" +}: InputModalProps) { + const [value, setValue] = useState(defaultValue); + + useEffect(() => { + if (open) { + setValue(defaultValue); + } + }, [open, defaultValue]); + + const handleConfirm = () => { + if (value.trim()) { + onConfirm(value.trim()); + onOpenChange(false); + setValue(""); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && value.trim()) { + e.preventDefault(); + handleConfirm(); + } + }; + + return ( + + + + {title} + {description && {description}} + + + + {label} + setValue(e.target.value)} + onKeyDown={handleKeyDown} + autoFocus + /> + + + + onOpenChange(false)}> + {cancelText} + + + {confirmText} + + + + + ); +} diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 9348abc..c83967f 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -24,6 +24,9 @@ import SettingsService, { Settings as AppSettings } from "@/services/SettingsSer import CalendarService, { CalendarEvent } from "@/services/CalendarService"; import { useNavigate } from "react-router-dom"; import { showError, showSuccess } from "@/utils/toast"; +import { ConfirmModal } from "@/components/modals/ConfirmModal"; +import { InputModal } from "@/components/modals/InputModal"; +import { AddUserModal } from "@/components/modals/AddUserModal"; interface UserType { id: number; @@ -47,6 +50,18 @@ const Index = () => { const [showArchived, setShowArchived] = useState(false); const navigate = useNavigate(); + // Modal states + const [deleteNoteModal, setDeleteNoteModal] = useState(false); + const [addTodoModal, setAddTodoModal] = useState(false); + const [addRssFeedModal, setAddRssFeedModal] = useState(false); + const [addUserModal, setAddUserModal] = useState(false); + const [deleteUserModal, setDeleteUserModal] = useState<{open: boolean, userId?: number}>({open: false}); + const [changePasswordModal, setChangePasswordModal] = useState<{open: boolean, userId?: number}>({open: false}); + const [addNoteTodoModal, setAddNoteTodoModal] = useState(false); + + // Debounce timer for auto-save + const saveTimerRef = useRef(null); + useEffect(() => { const userFromAuth = AuthService.getUser(); if (userFromAuth) { @@ -172,60 +187,89 @@ const Index = () => { setOpenNote(null); }; - const handleUpdateNote = async (updatedFields: Partial) => { + // Update note with immediate local state update + const handleUpdateNote = useCallback(async (updatedFields: Partial) => { if (!openNote) return; + const updatedNote = { + ...openNote, + ...updatedFields, + todos: updatedFields.todos || openNote.todos || [], + images: updatedFields.images || openNote.images || [], + files: updatedFields.files || openNote.files || [] + }; + + // Update local state immediately + setOpenNote(updatedNote); + setNotes(prev => prev.map(note => note.id === updatedNote.id ? updatedNote : note)); + try { - const updatedNote = { - ...openNote, - ...updatedFields, - todos: openNote.todos || [], - images: openNote.images || [], - files: openNote.files || [] - }; - const success = await NotesService.updateNote(updatedNote); - - if (success) { - setOpenNote(updatedNote); - setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note)); + if (!success) { + showError("Erreur lors de la sauvegarde"); } } catch (error) { showError("Erreur lors de la mise à jour de la note"); } - }; + }, [openNote, setOpenNote, setNotes]); - const handleDeleteNote = async () => { + // Debounced content update for typing + const handleContentChange = useCallback((content: string) => { + if (!openNote) return; + + // Update local state immediately for responsiveness + const updatedNote = { ...openNote, content }; + setOpenNote(updatedNote); + + // Debounce the API call + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + } + + saveTimerRef.current = setTimeout(async () => { + try { + await NotesService.updateNote(updatedNote); + } catch (error) { + showError("Erreur lors de la sauvegarde automatique"); + } + }, 1000); // Save after 1 second of inactivity + }, [openNote]); + + // Clean up timer on unmount + useEffect(() => { + return () => { + if (saveTimerRef.current) { + clearTimeout(saveTimerRef.current); + } + }; + }, []); + + const confirmDeleteNote = async () => { if (!openNote?.id) return; - if (window.confirm("Êtes-vous sûr de vouloir supprimer cette note ?")) { - try { - const success = await NotesService.deleteNote(openNote.id); + try { + const success = await NotesService.deleteNote(openNote.id); - if (success) { - const updatedNotes = notes.filter(note => note.id !== openNote.id); - setNotes(updatedNotes); - setOpenNote(null); - showSuccess("Note supprimée"); - } - } catch (error) { - showError("Erreur lors de la suppression"); + if (success) { + const updatedNotes = notes.filter(note => note.id !== openNote.id); + setNotes(updatedNotes); + setOpenNote(null); + showSuccess("Note supprimée"); } + } catch (error) { + showError("Erreur lors de la suppression"); } }; - const handleAddTodo = async () => { - const text = prompt("Nouvelle tâche:"); - if (text) { - try { - const newTodo = await TodosService.createTodo(text); - if (newTodo) { - setTodos([newTodo, ...todos]); - showSuccess("Tâche ajoutée"); - } - } catch (error) { - showError("Erreur lors de l'ajout de la tâche"); + const confirmAddTodo = async (text: string) => { + try { + const newTodo = await TodosService.createTodo(text); + if (newTodo) { + setTodos([newTodo, ...todos]); + showSuccess("Tâche ajoutée"); } + } catch (error) { + showError("Erreur lors de l'ajout de la tâche"); } }; @@ -248,19 +292,16 @@ const Index = () => { } }; - const handleAddRssFeed = async () => { - const url = prompt("URL du flux RSS:"); - if (url) { - try { - const newFeed = await RssService.addFeed(url); - if (newFeed) { - setRssFeeds([...rssFeeds, newFeed]); - showSuccess("Flux RSS ajouté"); - loadRssArticles(); - } - } catch (error) { - showError("Erreur lors de l'ajout du flux"); + const confirmAddRssFeed = async (url: string) => { + try { + const newFeed = await RssService.addFeed(url); + if (newFeed) { + setRssFeeds([...rssFeeds, newFeed]); + showSuccess("Flux RSS ajouté"); + loadRssArticles(); } + } catch (error) { + showError("Erreur lors de l'ajout du flux"); } }; @@ -285,13 +326,7 @@ const Index = () => { } }; - const handleAddUser = async () => { - const username = prompt("Nom d'utilisateur:"); - if (!username) return; - const password = prompt("Mot de passe:"); - if (!password) return; - const isAdmin = window.confirm("Donner les droits administrateur ?"); - + const confirmAddUser = async (username: string, password: string, isAdmin: boolean) => { try { const newUser = await AdminService.createUser(username, password, isAdmin); if (newUser) { @@ -303,31 +338,30 @@ const Index = () => { } }; - const handleDeleteUser = async (userId: number) => { - if (window.confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) { - try { - const success = await AdminService.deleteUser(userId); - if (success) { - loadUsers(); - showSuccess("Utilisateur supprimé"); - } - } catch (error) { - showError("Erreur lors de la suppression"); + const confirmDeleteUser = async () => { + if (!deleteUserModal.userId) return; + + try { + const success = await AdminService.deleteUser(deleteUserModal.userId); + if (success) { + loadUsers(); + showSuccess("Utilisateur supprimé"); } + } catch (error) { + showError("Erreur lors de la suppression"); } }; - const handleChangePassword = async (userId: number) => { - const newPassword = prompt("Nouveau mot de passe:"); - if (newPassword) { - try { - const success = await AdminService.updateUserPassword(userId, newPassword); - if (success) { - showSuccess("Mot de passe modifié"); - } - } catch (error) { - showError("Erreur lors de la modification"); + const confirmChangePassword = async (newPassword: string) => { + if (!changePasswordModal.userId) return; + + try { + const success = await AdminService.updateUserPassword(changePasswordModal.userId, newPassword); + if (success) { + showSuccess("Mot de passe modifié"); } + } catch (error) { + showError("Erreur lors de la modification"); } }; @@ -355,9 +389,11 @@ const Index = () => { }); if (response.ok) { - const file = await response.json(); - const updatedFiles = [...(openNote.files || []), file]; - handleUpdateNote({ files: updatedFiles }); + const result = await response.json(); + const newFile = result.file || result; + const updatedFiles = [...(openNote.files || []), newFile]; + setOpenNote({ ...openNote, files: updatedFiles }); + await handleUpdateNote({ files: updatedFiles }); showSuccess("Fichier ajouté"); } } catch (error) { @@ -548,7 +584,7 @@ const Index = () => { setDeleteNoteModal(true)} title="Supprimer" > @@ -590,19 +626,13 @@ const Index = () => { handleUpdateNote({ content })} + onChange={handleContentChange} /> { - const text = prompt("Nouvelle tâche:"); - if (text) { - const updatedTodos = [...(openNote.todos || []), { text, completed: false }]; - await handleUpdateNote({ todos: updatedTodos }); - } - }} + onClick={() => setAddNoteTodoModal(true)} className="w-full" > @@ -654,11 +684,14 @@ const Index = () => { const image = await NotesService.uploadImage(openNote.id, e.target.files[0]); if (image) { const updatedImages = [...(openNote.images || []), image]; + setOpenNote({ ...openNote, images: updatedImages }); await handleUpdateNote({ images: updatedImages }); + showSuccess("Image ajoutée"); } } catch (error) { showError("Erreur lors de l'upload de l'image"); } + e.target.value = ''; } }} className="cursor-pointer" @@ -780,7 +813,7 @@ const Index = () => { Tâches - + setAddTodoModal(true)} className="gap-2"> Ajouter @@ -867,7 +900,7 @@ const Index = () => { Actualiser - + setAddRssFeedModal(true)} className="gap-2"> Ajouter @@ -903,7 +936,7 @@ const Index = () => { setAddRssFeedModal(true)} > Ajouter un flux RSS @@ -1008,7 +1041,7 @@ const Index = () => { setAddUserModal(true)} > Ajouter un utilisateur @@ -1033,7 +1066,7 @@ const Index = () => { handleChangePassword(u.id)} + onClick={() => setChangePasswordModal({open: true, userId: u.id})} > Mot de passe @@ -1042,7 +1075,7 @@ const Index = () => { handleDeleteUser(u.id)} + onClick={() => setDeleteUserModal({open: true, userId: u.id})} > Supprimer @@ -1057,7 +1090,7 @@ const Index = () => { setAddRssFeedModal(true)} > Ajouter un flux RSS @@ -1228,6 +1261,81 @@ const Index = () => { + {/* Modals */} + + + + + + + + + setDeleteUserModal({open})} + title="Supprimer l'utilisateur" + description="Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible." + onConfirm={confirmDeleteUser} + confirmText="Supprimer" + variant="destructive" + /> + + setChangePasswordModal({open})} + title="Changer le mot de passe" + label="Nouveau mot de passe" + placeholder="Entrez le nouveau mot de passe..." + onConfirm={confirmChangePassword} + confirmText="Modifier" + type="password" + /> + + { + if (openNote) { + const updatedTodos = [...(openNote.todos || []), { text, completed: false }]; + await handleUpdateNote({ todos: updatedTodos }); + } + }} + confirmText="Ajouter" + /> + diff --git a/src/services/NotesService.ts b/src/services/NotesService.ts index a301ecb..03db3d9 100644 --- a/src/services/NotesService.ts +++ b/src/services/NotesService.ts @@ -130,7 +130,7 @@ class NotesService { async uploadImage(noteId: number, file: File): Promise { const formData = new FormData(); formData.append("image", file); - + try { const response = await fetch(`/api/notes/${noteId}/images`, { method: "POST", @@ -139,12 +139,13 @@ class NotesService { }, body: formData }); - + if (!response.ok) { throw new Error("Erreur lors de l'upload de l'image"); } - - return await response.json(); + + const result = await response.json(); + return result.image || result; } catch (error) { showError(error instanceof Error ? error.message : "Erreur serveur"); return null;