mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Merge pull request #33 from R0m1k3/claude/fix-npm-ci-build-error-011CV3c2K4ETHdBRQUs7tkJf
Claude/fix npm ci build error 011 cv3c2 k4 et hd brq us7tk jf
This commit is contained in:
6 files changed
+470
-100
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<UserPlus className="h-5 w-5" />
|
||||
Ajouter un utilisateur
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Créez un nouveau compte utilisateur pour l'application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">Nom d'utilisateur</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="Nom d'utilisateur"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Mot de passe</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Mot de passe"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="is-admin"
|
||||
checked={isAdmin}
|
||||
onCheckedChange={(checked) => setIsAdmin(checked as boolean)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="is-admin"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Administrateur
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={!isValid}>
|
||||
Créer l'utilisateur
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{variant === "destructive" && <AlertTriangle className="h-5 w-5 text-destructive" />}
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
variant={variant}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
{description && <DialogDescription>{description}</DialogDescription>}
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="input-field">{label}</Label>
|
||||
<Input
|
||||
id="input-field"
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={!value.trim()}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+204
-96
@@ -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<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const userFromAuth = AuthService.getUser();
|
||||
if (userFromAuth) {
|
||||
@@ -172,60 +187,89 @@ const Index = () => {
|
||||
setOpenNote(null);
|
||||
};
|
||||
|
||||
const handleUpdateNote = async (updatedFields: Partial<Note>) => {
|
||||
// Update note with immediate local state update
|
||||
const handleUpdateNote = useCallback(async (updatedFields: Partial<Note>) => {
|
||||
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 = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleDeleteNote}
|
||||
onClick={() => setDeleteNoteModal(true)}
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
@@ -590,19 +626,13 @@ const Index = () => {
|
||||
<TabsContent value="content" className="mt-4">
|
||||
<RichTextEditor
|
||||
content={openNote.content || ""}
|
||||
onChange={(content) => handleUpdateNote({ content })}
|
||||
onChange={handleContentChange}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="todos" className="mt-4 space-y-4">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
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"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
@@ -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 = () => {
|
||||
<CheckSquare className="h-6 w-6" />
|
||||
Tâches
|
||||
</CardTitle>
|
||||
<Button size="lg" variant="outline" onClick={handleAddTodo} className="gap-2">
|
||||
<Button size="lg" variant="outline" onClick={() => setAddTodoModal(true)} className="gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
Ajouter
|
||||
</Button>
|
||||
@@ -867,7 +900,7 @@ const Index = () => {
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Actualiser
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={handleAddRssFeed} className="gap-2">
|
||||
<Button size="lg" variant="outline" onClick={() => setAddRssFeedModal(true)} className="gap-2">
|
||||
<Plus className="h-5 w-5" />
|
||||
Ajouter
|
||||
</Button>
|
||||
@@ -903,7 +936,7 @@ const Index = () => {
|
||||
<Button
|
||||
variant="link"
|
||||
className="mt-2"
|
||||
onClick={handleAddRssFeed}
|
||||
onClick={() => setAddRssFeedModal(true)}
|
||||
>
|
||||
Ajouter un flux RSS
|
||||
</Button>
|
||||
@@ -1008,7 +1041,7 @@ const Index = () => {
|
||||
<TabsContent value="users" className="mt-4 space-y-4">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={handleAddUser}
|
||||
onClick={() => setAddUserModal(true)}
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
Ajouter un utilisateur
|
||||
@@ -1033,7 +1066,7 @@ const Index = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleChangePassword(u.id)}
|
||||
onClick={() => setChangePasswordModal({open: true, userId: u.id})}
|
||||
>
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
Mot de passe
|
||||
@@ -1042,7 +1075,7 @@ const Index = () => {
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(u.id)}
|
||||
onClick={() => setDeleteUserModal({open: true, userId: u.id})}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
@@ -1057,7 +1090,7 @@ const Index = () => {
|
||||
<TabsContent value="rss" className="mt-4 space-y-4">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={handleAddRssFeed}
|
||||
onClick={() => setAddRssFeedModal(true)}
|
||||
>
|
||||
<Rss className="h-4 w-4" />
|
||||
Ajouter un flux RSS
|
||||
@@ -1228,6 +1261,81 @@ const Index = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modals */}
|
||||
<ConfirmModal
|
||||
open={deleteNoteModal}
|
||||
onOpenChange={setDeleteNoteModal}
|
||||
title="Supprimer la note"
|
||||
description="Êtes-vous sûr de vouloir supprimer cette note ? Cette action est irréversible."
|
||||
onConfirm={confirmDeleteNote}
|
||||
confirmText="Supprimer"
|
||||
cancelText="Annuler"
|
||||
variant="destructive"
|
||||
/>
|
||||
|
||||
<InputModal
|
||||
open={addTodoModal}
|
||||
onOpenChange={setAddTodoModal}
|
||||
title="Nouvelle tâche"
|
||||
label="Tâche"
|
||||
placeholder="Entrez la tâche..."
|
||||
onConfirm={confirmAddTodo}
|
||||
confirmText="Ajouter"
|
||||
/>
|
||||
|
||||
<InputModal
|
||||
open={addRssFeedModal}
|
||||
onOpenChange={setAddRssFeedModal}
|
||||
title="Ajouter un flux RSS"
|
||||
label="URL du flux RSS"
|
||||
placeholder="https://example.com/feed.xml"
|
||||
onConfirm={confirmAddRssFeed}
|
||||
confirmText="Ajouter"
|
||||
type="url"
|
||||
/>
|
||||
|
||||
<AddUserModal
|
||||
open={addUserModal}
|
||||
onOpenChange={setAddUserModal}
|
||||
onConfirm={confirmAddUser}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
open={deleteUserModal.open}
|
||||
onOpenChange={(open) => 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"
|
||||
/>
|
||||
|
||||
<InputModal
|
||||
open={changePasswordModal.open}
|
||||
onOpenChange={(open) => 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"
|
||||
/>
|
||||
|
||||
<InputModal
|
||||
open={addNoteTodoModal}
|
||||
onOpenChange={setAddNoteTodoModal}
|
||||
title="Nouvelle tâche"
|
||||
label="Tâche"
|
||||
placeholder="Entrez la tâche..."
|
||||
onConfirm={async (text) => {
|
||||
if (openNote) {
|
||||
const updatedTodos = [...(openNote.todos || []), { text, completed: false }];
|
||||
await handleUpdateNote({ todos: updatedTodos });
|
||||
}
|
||||
}}
|
||||
confirmText="Ajouter"
|
||||
/>
|
||||
|
||||
<div className="fixed bottom-4 right-4">
|
||||
<MadeWithDyad />
|
||||
</div>
|
||||
|
||||
@@ -130,7 +130,7 @@ class NotesService {
|
||||
async uploadImage(noteId: number, file: File): Promise<Image | null> {
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user