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:
LogiFlow authored and GitHub committed 2025-11-12 10:28:52 +01:00
commit 7d307a2e1f
6 files changed
+470 -100

No files matched your search

+20
View File
@@ -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;
+95
View File
@@ -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>
);
}
+57
View File
@@ -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>
);
}
+89
View File
@@ -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
View File
@@ -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>
+5 -4
View File
@@ -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;