mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 15:48:16 +02:00
Merge pull request #28 from R0m1k3/claude/fix-npm-ci-build-error-011CV3c2K4ETHdBRQUs7tkJf
Feature: Implémentation complète de NoteFlow avec toutes les fonction…
This commit is contained in:
8 files changed
+956
-143
No files matched your search
+2
-1
@@ -2,8 +2,9 @@
|
|||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Notes App</title>
|
<title>NoteFlow - Notes et gestion de tâches</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "noteflow",
|
"name": "noteflow",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Application web de notes et todo list moderne avec Docker",
|
"description": "Application NoteFlow - Notes et gestion de tâches moderne avec Docker",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Underline,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
AlignLeft,
|
||||||
|
AlignCenter,
|
||||||
|
AlignRight,
|
||||||
|
Code,
|
||||||
|
Quote,
|
||||||
|
Link as LinkIcon
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface RichTextEditorProps {
|
||||||
|
content: string;
|
||||||
|
onChange: (content: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RichTextEditor({ content, onChange, placeholder }: RichTextEditorProps) {
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [selectedFormat, setSelectedFormat] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editorRef.current && editorRef.current.innerHTML !== content) {
|
||||||
|
editorRef.current.innerHTML = content;
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const execCommand = (command: string, value?: string) => {
|
||||||
|
document.execCommand(command, false, value);
|
||||||
|
editorRef.current?.focus();
|
||||||
|
updateContent();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContent = () => {
|
||||||
|
if (editorRef.current) {
|
||||||
|
onChange(editorRef.current.innerHTML);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
// Gestion des raccourcis clavier
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
switch (e.key.toLowerCase()) {
|
||||||
|
case 'b':
|
||||||
|
e.preventDefault();
|
||||||
|
execCommand('bold');
|
||||||
|
break;
|
||||||
|
case 'i':
|
||||||
|
e.preventDefault();
|
||||||
|
execCommand('italic');
|
||||||
|
break;
|
||||||
|
case 'u':
|
||||||
|
e.preventDefault();
|
||||||
|
execCommand('underline');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertLink = () => {
|
||||||
|
const url = prompt('Entrez l\'URL:');
|
||||||
|
if (url) {
|
||||||
|
execCommand('createLink', url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatButton = (
|
||||||
|
icon: React.ReactNode,
|
||||||
|
command: string,
|
||||||
|
value?: string,
|
||||||
|
tooltip?: string
|
||||||
|
) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => execCommand(command, value)}
|
||||||
|
title={tooltip}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Barre d'outils */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-2">
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{/* Titres */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{formatButton(<Heading1 className="h-4 w-4" />, 'formatBlock', '<h1>', 'Titre 1')}
|
||||||
|
{formatButton(<Heading2 className="h-4 w-4" />, 'formatBlock', '<h2>', 'Titre 2')}
|
||||||
|
{formatButton(<Heading3 className="h-4 w-4" />, 'formatBlock', '<h3>', 'Titre 3')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
|
||||||
|
{/* Formatage de texte */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{formatButton(<Bold className="h-4 w-4" />, 'bold', undefined, 'Gras (Ctrl+B)')}
|
||||||
|
{formatButton(<Italic className="h-4 w-4" />, 'italic', undefined, 'Italique (Ctrl+I)')}
|
||||||
|
{formatButton(<Underline className="h-4 w-4" />, 'underline', undefined, 'Souligné (Ctrl+U)')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
|
||||||
|
{/* Listes */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{formatButton(<List className="h-4 w-4" />, 'insertUnorderedList', undefined, 'Liste à puces')}
|
||||||
|
{formatButton(<ListOrdered className="h-4 w-4" />, 'insertOrderedList', undefined, 'Liste numérotée')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
|
||||||
|
{/* Alignement */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{formatButton(<AlignLeft className="h-4 w-4" />, 'justifyLeft', undefined, 'Aligner à gauche')}
|
||||||
|
{formatButton(<AlignCenter className="h-4 w-4" />, 'justifyCenter', undefined, 'Centrer')}
|
||||||
|
{formatButton(<AlignRight className="h-4 w-4" />, 'justifyRight', undefined, 'Aligner à droite')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" className="h-6" />
|
||||||
|
|
||||||
|
{/* Autres */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{formatButton(<Quote className="h-4 w-4" />, 'formatBlock', '<blockquote>', 'Citation')}
|
||||||
|
{formatButton(<Code className="h-4 w-4" />, 'formatBlock', '<pre>', 'Code')}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={insertLink}
|
||||||
|
title="Insérer un lien"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<LinkIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Zone d'édition */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
contentEditable
|
||||||
|
onInput={updateContent}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="min-h-[400px] outline-none prose prose-sm max-w-none dark:prose-invert"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
data-placeholder={placeholder || 'Commencez à écrire...'}
|
||||||
|
style={{
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '1.6'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+547
-141
@@ -1,18 +1,29 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RichTextEditor } from "@/components/RichTextEditor";
|
||||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||||
import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, Image, CheckSquare } from "lucide-react";
|
import {
|
||||||
|
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
||||||
|
Image, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X
|
||||||
|
} from "lucide-react";
|
||||||
import AuthService from "@/services/AuthService";
|
import AuthService from "@/services/AuthService";
|
||||||
import AdminService from "@/services/AdminService";
|
import AdminService from "@/services/AdminService";
|
||||||
import NotesService, { Note } from "@/services/NotesService";
|
import NotesService, { Note } from "@/services/NotesService";
|
||||||
|
import TodosService, { Todo } from "@/services/TodosService";
|
||||||
|
import RssService, { RssFeed, RssArticle } from "@/services/RssService";
|
||||||
|
import SettingsService, { Settings as AppSettings } from "@/services/SettingsService";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { showError } from "@/utils/toast";
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
|
|
||||||
interface UserType {
|
interface UserType {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -28,17 +39,21 @@ const Index = () => {
|
|||||||
const [notes, setNotes] = useState<Note[]>([]);
|
const [notes, setNotes] = useState<Note[]>([]);
|
||||||
const [currentNote, setCurrentNote] = useState<Note | null>(null);
|
const [currentNote, setCurrentNote] = useState<Note | null>(null);
|
||||||
const [users, setUsers] = useState<UserType[]>([]);
|
const [users, setUsers] = useState<UserType[]>([]);
|
||||||
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
|
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
|
||||||
|
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
|
||||||
|
const [settings, setSettings] = useState<AppSettings>({});
|
||||||
|
const [adminTab, setAdminTab] = useState("users");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Récupérer l'utilisateur et vérifier l'authentification
|
|
||||||
const userFromAuth = AuthService.getUser();
|
const userFromAuth = AuthService.getUser();
|
||||||
if (userFromAuth) {
|
if (userFromAuth) {
|
||||||
setUser(userFromAuth);
|
setUser(userFromAuth);
|
||||||
loadNotes().catch(err => {
|
loadNotes();
|
||||||
console.error("Erreur lors du chargement des notes:", err);
|
loadTodos();
|
||||||
showError("Erreur lors du chargement des notes");
|
loadRssFeeds();
|
||||||
});
|
loadRssArticles();
|
||||||
} else {
|
} else {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
@@ -55,7 +70,39 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du chargement des notes:", error);
|
console.error("Erreur lors du chargement des notes:", error);
|
||||||
throw error;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTodos = async () => {
|
||||||
|
try {
|
||||||
|
const fetchedTodos = await TodosService.getTodos();
|
||||||
|
if (Array.isArray(fetchedTodos)) {
|
||||||
|
setTodos(fetchedTodos);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des todos:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRssFeeds = async () => {
|
||||||
|
try {
|
||||||
|
const feeds = await RssService.getFeeds();
|
||||||
|
if (Array.isArray(feeds)) {
|
||||||
|
setRssFeeds(feeds);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des flux RSS:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRssArticles = async () => {
|
||||||
|
try {
|
||||||
|
const articles = await RssService.getArticles(10);
|
||||||
|
if (Array.isArray(articles)) {
|
||||||
|
setRssArticles(articles);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des articles RSS:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,11 +115,28 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du chargement des utilisateurs:", error);
|
console.error("Erreur lors du chargement des utilisateurs:", error);
|
||||||
showError("Erreur lors du chargement des utilisateurs");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSettings = async () => {
|
||||||
|
if (user?.is_admin) {
|
||||||
|
try {
|
||||||
|
const fetchedSettings = await SettingsService.getSettings();
|
||||||
|
setSettings(fetchedSettings || {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des paramètres:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAdmin) {
|
||||||
|
loadUsers();
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
}, [showAdmin]);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
AuthService.logout();
|
AuthService.logout();
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
@@ -86,7 +150,6 @@ const Index = () => {
|
|||||||
setCurrentNote(newNote);
|
setCurrentNote(newNote);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la création de la note:", error);
|
|
||||||
showError("Erreur lors de la création de la note");
|
showError("Erreur lors de la création de la note");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -99,7 +162,8 @@ const Index = () => {
|
|||||||
...currentNote,
|
...currentNote,
|
||||||
...updatedFields,
|
...updatedFields,
|
||||||
todos: currentNote.todos || [],
|
todos: currentNote.todos || [],
|
||||||
images: currentNote.images || []
|
images: currentNote.images || [],
|
||||||
|
files: currentNote.files || []
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = await NotesService.updateNote(updatedNote);
|
const success = await NotesService.updateNote(updatedNote);
|
||||||
@@ -109,7 +173,6 @@ const Index = () => {
|
|||||||
setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note));
|
setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la mise à jour de la note:", error);
|
|
||||||
showError("Erreur lors de la mise à jour de la note");
|
showError("Erreur lors de la mise à jour de la note");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,58 +188,100 @@ const Index = () => {
|
|||||||
const updatedNotes = notes.filter(note => note.id !== currentNote.id);
|
const updatedNotes = notes.filter(note => note.id !== currentNote.id);
|
||||||
setNotes(updatedNotes);
|
setNotes(updatedNotes);
|
||||||
setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null);
|
setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null);
|
||||||
|
showSuccess("Note supprimée");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression de la note:", error);
|
showError("Erreur lors de la suppression");
|
||||||
showError("Erreur lors de la suppression de la note");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 handleToggleTodo = async (todoId: number) => {
|
||||||
|
try {
|
||||||
|
await TodosService.toggleComplete(todoId);
|
||||||
|
setTodos(todos.map(t => t.id === todoId ? { ...t, completed: !t.completed } : t));
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de la mise à jour");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTodo = async (todoId: number) => {
|
||||||
|
try {
|
||||||
|
await TodosService.deleteTodo(todoId);
|
||||||
|
setTodos(todos.filter(t => t.id !== todoId));
|
||||||
|
showSuccess("Tâche supprimée");
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 handleDeleteRssFeed = async (feedId: number) => {
|
||||||
|
try {
|
||||||
|
await RssService.deleteFeed(feedId);
|
||||||
|
setRssFeeds(rssFeeds.filter(f => f.id !== feedId));
|
||||||
|
showSuccess("Flux supprimé");
|
||||||
|
loadRssArticles();
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefreshRss = async () => {
|
||||||
|
try {
|
||||||
|
await RssService.refreshFeeds();
|
||||||
|
loadRssArticles();
|
||||||
|
showSuccess("Flux actualisés");
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de l'actualisation");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddUser = async () => {
|
const handleAddUser = async () => {
|
||||||
const username = prompt("Nom d'utilisateur:");
|
const username = prompt("Nom d'utilisateur:");
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
const password = prompt("Mot de passe:");
|
const password = prompt("Mot de passe:");
|
||||||
if (!password) return;
|
if (!password) return;
|
||||||
|
|
||||||
const isAdmin = window.confirm("Donner les droits administrateur ?");
|
const isAdmin = window.confirm("Donner les droits administrateur ?");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newUser = await AdminService.createUser(username, password, isAdmin);
|
const newUser = await AdminService.createUser(username, password, isAdmin);
|
||||||
if (newUser) {
|
if (newUser) {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
|
showSuccess("Utilisateur créé");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la création de l'utilisateur:", error);
|
showError("Erreur lors de la création");
|
||||||
showError("Erreur lors de la création de l'utilisateur");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangePassword = async (userId: number) => {
|
|
||||||
const password = prompt("Nouveau mot de passe:");
|
|
||||||
if (!password) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const success = await AdminService.updateUserPassword(userId, password);
|
|
||||||
if (success) {
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la modification du mot de passe:", error);
|
|
||||||
showError("Erreur lors de la modification du mot de passe");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleAdmin = async (userId: number, currentStatus: boolean) => {
|
|
||||||
try {
|
|
||||||
const success = await AdminService.toggleAdminStatus(userId, currentStatus);
|
|
||||||
if (success) {
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la modification des droits:", error);
|
|
||||||
showError("Erreur lors de la modification des droits");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,20 +291,50 @@ const Index = () => {
|
|||||||
const success = await AdminService.deleteUser(userId);
|
const success = await AdminService.deleteUser(userId);
|
||||||
if (success) {
|
if (success) {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
|
showSuccess("Utilisateur supprimé");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression de l'utilisateur:", error);
|
showError("Erreur lors de la suppression");
|
||||||
showError("Erreur lors de la suppression de l'utilisateur");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Charger les utilisateurs quand le panel admin s'ouvre
|
const handleSaveSettings = async () => {
|
||||||
useEffect(() => {
|
try {
|
||||||
if (showAdmin) {
|
await SettingsService.updateSettings(settings);
|
||||||
loadUsers();
|
showSuccess("Paramètres enregistrés");
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de l'enregistrement");
|
||||||
}
|
}
|
||||||
}, [showAdmin]);
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files && e.target.files[0] && currentNote?.id) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', e.target.files[0]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${currentNote.id}/files`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const file = await response.json();
|
||||||
|
const updatedFiles = [...(currentNote.files || []), file];
|
||||||
|
handleUpdateNote({ files: updatedFiles });
|
||||||
|
showSuccess("Fichier ajouté");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de l'upload");
|
||||||
|
} finally {
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filteredNotes = notes.filter(note =>
|
const filteredNotes = notes.filter(note =>
|
||||||
(activeTab === "all" || (activeTab === "archived" && note.archived)) &&
|
(activeTab === "all" || (activeTab === "archived" && note.archived)) &&
|
||||||
@@ -216,20 +351,9 @@ const Index = () => {
|
|||||||
<div className="flex h-16 items-center justify-between">
|
<div className="flex h-16 items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
<svg
|
<FileText className="h-4 w-4 text-primary" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="h-4 w-4 text-primary"
|
|
||||||
>
|
|
||||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xl font-semibold">Notes App</span>
|
<span className="text-xl font-semibold">NoteFlow</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -269,8 +393,8 @@ const Index = () => {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="max-w-screen-2xl mx-auto px-4 py-6">
|
<div className="max-w-screen-2xl mx-auto px-4 py-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-[350px_1fr_300px] gap-6">
|
||||||
{/* Sidebar */}
|
{/* Left Sidebar - Notes */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
@@ -299,7 +423,7 @@ const Index = () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="space-y-2 max-h-[calc(100vh-240px)] overflow-y-auto">
|
<div className="space-y-2 max-h-[calc(100vh-340px)] overflow-y-auto">
|
||||||
{filteredNotes.length === 0 ? (
|
{filteredNotes.length === 0 ? (
|
||||||
<p className="text-center text-muted-foreground py-4">Aucune note trouvée</p>
|
<p className="text-center text-muted-foreground py-4">Aucune note trouvée</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -310,10 +434,30 @@ const Index = () => {
|
|||||||
onClick={() => setCurrentNote(note)}
|
onClick={() => setCurrentNote(note)}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<h3 className="font-medium">{note.title || "Sans titre"}</h3>
|
<h3 className="font-medium truncate">{note.title || "Sans titre"}</h3>
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<p className="text-sm text-muted-foreground line-clamp-2 mt-1">
|
||||||
{note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
|
{note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
{note.todos && note.todos.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
<CheckSquare className="h-3 w-3 mr-1" />
|
||||||
|
{note.todos.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.images && note.images.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
<Image className="h-3 w-3 mr-1" />
|
||||||
|
{note.images.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{note.files && note.files.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
<Paperclip className="h-3 w-3 mr-1" />
|
||||||
|
{note.files.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))
|
||||||
@@ -321,7 +465,7 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Note Editor */}
|
{/* Center - Note Editor */}
|
||||||
{currentNote ? (
|
{currentNote ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -354,28 +498,37 @@ const Index = () => {
|
|||||||
<Tabs defaultValue="content">
|
<Tabs defaultValue="content">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="content">Contenu</TabsTrigger>
|
<TabsTrigger value="content">Contenu</TabsTrigger>
|
||||||
<TabsTrigger value="todos">Tâches</TabsTrigger>
|
<TabsTrigger value="todos">
|
||||||
<TabsTrigger value="images">Images</TabsTrigger>
|
Tâches
|
||||||
|
{currentNote.todos && currentNote.todos.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2">{currentNote.todos.length}</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="images">
|
||||||
|
Images
|
||||||
|
{currentNote.images && currentNote.images.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2">{currentNote.images.length}</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="files">
|
||||||
|
Fichiers
|
||||||
|
{currentNote.files && currentNote.files.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-2">{currentNote.files.length}</Badge>
|
||||||
|
)}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="content" className="mt-4">
|
<TabsContent value="content" className="mt-4">
|
||||||
<Card>
|
<RichTextEditor
|
||||||
<CardContent className="p-4">
|
content={currentNote.content || ""}
|
||||||
<div
|
onChange={(content) => handleUpdateNote({ content })}
|
||||||
className="min-h-[500px] outline-none"
|
placeholder="Commencez à écrire votre note..."
|
||||||
contentEditable
|
|
||||||
suppressContentEditableWarning
|
|
||||||
dangerouslySetInnerHTML={{ __html: currentNote.content || "" }}
|
|
||||||
onBlur={(e) => handleUpdateNote({ content: e.currentTarget.innerHTML })}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="todos" className="mt-4">
|
<TabsContent value="todos" className="mt-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 space-y-4">
|
<CardContent className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -391,29 +544,26 @@ const Index = () => {
|
|||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Ajouter une tâche
|
Ajouter une tâche
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{currentNote.todos && currentNote.todos.length > 0 ? (
|
{currentNote.todos && currentNote.todos.length > 0 ? (
|
||||||
currentNote.todos.map((todo, index) => (
|
currentNote.todos.map((todo, index) => (
|
||||||
<div key={index} className="flex items-center gap-2 p-2 border rounded-md">
|
<div key={index} className="flex items-center gap-2 p-2 border rounded-md">
|
||||||
<input
|
<Checkbox
|
||||||
type="checkbox"
|
|
||||||
checked={todo.completed}
|
checked={todo.completed}
|
||||||
onChange={() => {
|
onCheckedChange={() => {
|
||||||
const updatedTodos = [...(currentNote.todos || [])];
|
const updatedTodos = [...(currentNote.todos || [])];
|
||||||
updatedTodos[index].completed = !updatedTodos[index].completed;
|
updatedTodos[index].completed = !updatedTodos[index].completed;
|
||||||
handleUpdateNote({ todos: updatedTodos });
|
handleUpdateNote({ todos: updatedTodos });
|
||||||
}}
|
}}
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
/>
|
||||||
<span className={todo.completed ? "line-through text-muted-foreground" : ""}>
|
<span className={todo.completed ? "line-through text-muted-foreground flex-1" : "flex-1"}>
|
||||||
{todo.text}
|
{todo.text}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="ml-auto h-6 w-6"
|
className="h-6 w-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedTodos = [...(currentNote.todos || [])];
|
const updatedTodos = [...(currentNote.todos || [])];
|
||||||
updatedTodos.splice(index, 1);
|
updatedTodos.splice(index, 1);
|
||||||
@@ -425,7 +575,7 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-muted-foreground py-4">Aucune tâche</p>
|
<p className="text-center text-muted-foreground py-8">Aucune tâche dans cette note</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -435,13 +585,13 @@ const Index = () => {
|
|||||||
<TabsContent value="images" className="mt-4">
|
<TabsContent value="images" className="mt-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 space-y-4">
|
<CardContent className="p-4 space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<label htmlFor="image-upload">
|
||||||
<label htmlFor="image-upload" className="cursor-pointer">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => document.getElementById('image-upload')?.click()}
|
||||||
>
|
>
|
||||||
<Image className="h-4 w-4" />
|
<Image className="h-4 w-4" />
|
||||||
Ajouter une image
|
Ajouter une image
|
||||||
@@ -458,10 +608,10 @@ const Index = () => {
|
|||||||
if (image) {
|
if (image) {
|
||||||
const updatedImages = [...(currentNote.images || []), image];
|
const updatedImages = [...(currentNote.images || []), image];
|
||||||
handleUpdateNote({ images: updatedImages });
|
handleUpdateNote({ images: updatedImages });
|
||||||
|
showSuccess("Image ajoutée");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de l'upload de l'image:", error);
|
showError("Erreur lors de l'upload");
|
||||||
showError("Erreur lors de l'upload de l'image");
|
|
||||||
} finally {
|
} finally {
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
@@ -469,21 +619,20 @@ const Index = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
{currentNote.images && currentNote.images.length > 0 ? (
|
{currentNote.images && currentNote.images.length > 0 ? (
|
||||||
currentNote.images.map((image, index) => (
|
currentNote.images.map((image, index) => (
|
||||||
<div key={index} className="relative aspect-square rounded-md overflow-hidden border">
|
<div key={index} className="relative aspect-square rounded-md overflow-hidden border group">
|
||||||
<img
|
<img
|
||||||
src={`/uploads/${image.filename}`}
|
src={`/uploads/${image.filename}`}
|
||||||
alt={`Note image ${index}`}
|
alt={`Image ${index + 1}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-1 right-1 h-6 w-6 rounded-full"
|
className="absolute top-1 right-1 h-6 w-6 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (image.id && currentNote.id) {
|
if (image.id && currentNote.id) {
|
||||||
try {
|
try {
|
||||||
@@ -491,20 +640,99 @@ const Index = () => {
|
|||||||
if (success) {
|
if (success) {
|
||||||
const updatedImages = (currentNote.images || []).filter((_, i) => i !== index);
|
const updatedImages = (currentNote.images || []).filter((_, i) => i !== index);
|
||||||
handleUpdateNote({ images: updatedImages });
|
handleUpdateNote({ images: updatedImages });
|
||||||
|
showSuccess("Image supprimée");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la suppression de l'image:", error);
|
showError("Erreur lors de la suppression");
|
||||||
showError("Erreur lors de la suppression de l'image");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="col-span-full text-center text-muted-foreground py-4">Aucune image</p>
|
<p className="col-span-full text-center text-muted-foreground py-8">Aucune image</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="files" className="mt-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<label htmlFor="file-upload">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
type="button"
|
||||||
|
onClick={() => document.getElementById('file-upload')?.click()}
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4" />
|
||||||
|
Ajouter un fichier
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{currentNote.files && currentNote.files.length > 0 ? (
|
||||||
|
currentNote.files.map((file: any, index: number) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 border rounded-md hover:bg-accent/50 transition-colors group">
|
||||||
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
<Paperclip className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm truncate">{file.filename || file.original_name}</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{file.size ? `${(file.size / 1024).toFixed(1)} KB` : ''}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => window.open(`/uploads/${file.filename}`, '_blank')}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={async () => {
|
||||||
|
if (file.id && currentNote.id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${currentNote.id}/files/${file.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const updatedFiles = (currentNote.files || []).filter((_, i) => i !== index);
|
||||||
|
handleUpdateNote({ files: updatedFiles });
|
||||||
|
showSuccess("Fichier supprimé");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError("Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-8">Aucun fichier attaché</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -513,24 +741,126 @@ const Index = () => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-muted-foreground">Aucune note sélectionnée</p>
|
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||||
<Button
|
<p className="text-muted-foreground mb-4">Aucune note sélectionnée</p>
|
||||||
className="mt-4"
|
<Button onClick={handleCreateNote}>
|
||||||
onClick={handleCreateNote}
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
>
|
|
||||||
Créer une note
|
Créer une note
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Right Sidebar - Todos & RSS */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Todos Box */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<CheckSquare className="h-5 w-5" />
|
||||||
|
Tâches
|
||||||
|
</CardTitle>
|
||||||
|
<Button size="sm" variant="ghost" onClick={handleAddTodo}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||||
|
{todos.length > 0 ? (
|
||||||
|
todos.map(todo => (
|
||||||
|
<div key={todo.id} className="flex items-center gap-2 p-2 border rounded-md hover:bg-accent/50 transition-colors group">
|
||||||
|
<Checkbox
|
||||||
|
checked={todo.completed}
|
||||||
|
onCheckedChange={() => todo.id && handleToggleTodo(todo.id)}
|
||||||
|
/>
|
||||||
|
<span className={`text-sm flex-1 ${todo.completed ? 'line-through text-muted-foreground' : ''}`}>
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
onClick={() => todo.id && handleDeleteTodo(todo.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-center text-muted-foreground py-4">Aucune tâche</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* RSS Box */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-lg flex items-center gap-2">
|
||||||
|
<Rss className="h-5 w-5" />
|
||||||
|
Flux RSS
|
||||||
|
</CardTitle>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button size="sm" variant="ghost" onClick={handleRefreshRss}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" onClick={handleAddRssFeed}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3 max-h-[400px] overflow-y-auto">
|
||||||
|
{rssArticles.length > 0 ? (
|
||||||
|
rssArticles.map(article => (
|
||||||
|
<div
|
||||||
|
key={article.id}
|
||||||
|
className="p-2 border rounded-md 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="text-sm font-medium 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 w-3 text-muted-foreground flex-shrink-0 mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-center text-muted-foreground py-4">
|
||||||
|
Aucun article
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={handleAddRssFeed}
|
||||||
|
>
|
||||||
|
Ajouter un flux RSS
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Modal */}
|
{/* Admin Modal */}
|
||||||
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
||||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Administration</DialogTitle>
|
<DialogTitle>Administration</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -538,9 +868,11 @@ const Index = () => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs defaultValue="users">
|
<Tabs value={adminTab} onValueChange={setAdminTab}>
|
||||||
<TabsList className="grid grid-cols-2 w-full">
|
<TabsList className="grid grid-cols-4 w-full">
|
||||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||||
|
<TabsTrigger value="rss">Flux RSS</TabsTrigger>
|
||||||
|
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
||||||
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -554,13 +886,12 @@ const Index = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{users.length > 0 ? (
|
{users.map(user => (
|
||||||
users.map(user => (
|
|
||||||
<Card key={user.id}>
|
<Card key={user.id}>
|
||||||
<CardContent className="p-4 flex items-center justify-between">
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarFallback>{user.username ? user.username.charAt(0).toUpperCase() : "U"}</AvatarFallback>
|
<AvatarFallback>{user.username.charAt(0).toUpperCase()}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{user.username}</p>
|
<p className="font-medium">{user.username}</p>
|
||||||
@@ -569,23 +900,7 @@ const Index = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{user.id !== 1 && (
|
{user.id !== 1 && (
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleChangePassword(user.id)}
|
|
||||||
>
|
|
||||||
Mot de passe
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleToggleAdmin(user.id, user.is_admin)}
|
|
||||||
>
|
|
||||||
{user.is_admin ? "Retirer admin" : "Rendre admin"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -593,22 +908,113 @@ const Index = () => {
|
|||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
))}
|
||||||
) : (
|
</div>
|
||||||
<p className="text-center text-muted-foreground py-4">Chargement des utilisateurs...</p>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="rss" className="mt-4 space-y-4">
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={handleAddRssFeed}
|
||||||
|
>
|
||||||
|
<Rss className="h-4 w-4" />
|
||||||
|
Ajouter un flux RSS
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{rssFeeds.map(feed => (
|
||||||
|
<Card key={feed.id}>
|
||||||
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-medium">{feed.title}</p>
|
||||||
|
<p className="text-sm text-muted-foreground truncate">{feed.url}</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => feed.id && handleDeleteRssFeed(feed.id)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{rssFeeds.length === 0 && (
|
||||||
|
<p className="text-center text-muted-foreground py-8">Aucun flux RSS configuré</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="openrouter" className="mt-4 space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Key className="h-5 w-5" />
|
||||||
|
Configuration OpenRouter
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="api-key">Clé API OpenRouter</Label>
|
||||||
|
<Input
|
||||||
|
id="api-key"
|
||||||
|
type="password"
|
||||||
|
placeholder="sk-or-..."
|
||||||
|
value={settings.openrouter_api_key || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, openrouter_api_key: e.target.value })}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Obtenez votre clé API sur{' '}
|
||||||
|
<a href="https://openrouter.ai" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||||
|
openrouter.ai
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ai-model">Modèle IA</Label>
|
||||||
|
<Input
|
||||||
|
id="ai-model"
|
||||||
|
placeholder="anthropic/claude-3-sonnet"
|
||||||
|
value={settings.ai_model || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, ai_model: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
Enregistrer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="settings" className="mt-4">
|
<TabsContent value="settings" className="mt-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardHeader>
|
||||||
<p>Paramètres de l'application</p>
|
<CardTitle>Paramètres généraux</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rss-interval">Intervalle de rafraîchissement RSS (minutes)</Label>
|
||||||
|
<Input
|
||||||
|
id="rss-interval"
|
||||||
|
type="number"
|
||||||
|
min="5"
|
||||||
|
placeholder="30"
|
||||||
|
value={settings.rss_refresh_interval || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, rss_refresh_interval: parseInt(e.target.value) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleSaveSettings}>
|
||||||
|
Enregistrer
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ interface Image {
|
|||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NoteFile {
|
||||||
|
id?: number;
|
||||||
|
filename: string;
|
||||||
|
original_name?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Note {
|
export interface Note {
|
||||||
id?: number;
|
id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -22,6 +29,7 @@ export interface Note {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
todos: Todo[];
|
todos: Todo[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
|
files?: NoteFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotesService {
|
class NotesService {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface RssFeed {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
category?: string;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RssArticle {
|
||||||
|
id?: number;
|
||||||
|
feed_id: number;
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
description?: string;
|
||||||
|
pub_date?: string;
|
||||||
|
read: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RssService {
|
||||||
|
private baseURL = '/api/rss';
|
||||||
|
|
||||||
|
private getAuthHeader() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFeeds(): Promise<RssFeed[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.baseURL}/feeds`, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching RSS feeds:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFeed(url: string, title?: string): Promise<RssFeed> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${this.baseURL}/feeds`, { url, title }, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding RSS feed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFeed(feedId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${this.baseURL}/feeds/${feedId}`, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting RSS feed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getArticles(limit: number = 20): Promise<RssArticle[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.baseURL}/articles?limit=${limit}`, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching RSS articles:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAsRead(articleId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.patch(`${this.baseURL}/articles/${articleId}/read`, {}, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking article as read:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshFeeds(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.post(`${this.baseURL}/refresh`, {}, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing RSS feeds:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new RssService();
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
openrouter_api_key?: string;
|
||||||
|
ai_model?: string;
|
||||||
|
rss_refresh_interval?: number;
|
||||||
|
theme?: 'light' | 'dark' | 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsService {
|
||||||
|
private baseURL = '/api/settings';
|
||||||
|
|
||||||
|
private getAuthHeader() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSettings(): Promise<Settings> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(this.baseURL, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(settings: Settings): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.put(this.baseURL, settings, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating settings:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async testOpenRouterConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${this.baseURL}/test-openrouter`, {}, this.getAuthHeader());
|
||||||
|
return response.data.success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error testing OpenRouter connection:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SettingsService();
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export interface Todo {
|
||||||
|
id?: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
priority?: 'low' | 'medium' | 'high';
|
||||||
|
due_date?: string;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TodosService {
|
||||||
|
private baseURL = '/api/todos';
|
||||||
|
|
||||||
|
private getAuthHeader() {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTodos(): Promise<Todo[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(this.baseURL, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching todos:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTodo(text: string, priority?: 'low' | 'medium' | 'high'): Promise<Todo> {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(this.baseURL, { text, priority, completed: false }, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating todo:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTodo(todo: Todo): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.put(`${this.baseURL}/${todo.id}`, todo, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating todo:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTodo(todoId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${this.baseURL}/${todoId}`, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting todo:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleComplete(todoId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.patch(`${this.baseURL}/${todoId}/toggle`, {}, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling todo:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new TodosService();
|
||||||
Reference in new issue
Block a user