mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Feature: Implémentation complète de NoteFlow avec toutes les fonctionnalités
Cette mise à jour majeure transforme l'application en une plateforme complète de gestion de notes et de tâches avec de nombreuses nouvelles fonctionnalités. Nouvelles fonctionnalités: 1. **Éditeur de texte enrichi** - Barre d'outils complète avec formatage (gras, italique, souligné) - Support des titres (H1, H2, H3) - Listes à puces et numérotées - Alignement du texte (gauche, centre, droite) - Citations et blocs de code - Insertion de liens - Raccourcis clavier (Ctrl+B, Ctrl+I, Ctrl+U) 2. **Box Tâches globales (sidebar droite)** - Liste de tâches accessible depuis toute l'application - Ajout/suppression de tâches - Marquage des tâches comme complétées - Persistance des données 3. **Box Flux RSS (sidebar droite)** - Affichage des derniers articles RSS - Rafraîchissement manuel des flux - Ouverture des articles dans un nouvel onglet - Gestion des flux dans l'admin 4. **Support des fichiers joints dans les notes** - Upload de tous types de fichiers - Affichage de la taille des fichiers - Téléchargement et suppression des fichiers - Interface utilisateur intuitive 5. **Panel d'administration enrichi** - Onglet "Flux RSS" : gestion des flux RSS - Onglet "OpenRouter" : configuration de l'API IA - Onglet "Paramètres" : configuration de l'intervalle de rafraîchissement RSS - Interface organisée avec 4 onglets 6. **Amélioration de l'interface** - Layout à 3 colonnes responsive (Notes | Éditeur | Tâches+RSS) - Badges pour indiquer le nombre de tâches, images et fichiers par note - Design moderne et cohérent - Animations et transitions fluides 7. **Mise à jour de l'identité** - Nom de l'application : "NoteFlow" - Nouveau titre de page - Favicon SVG configuré - Logo mis à jour dans l'interface Services ajoutés: - RssService.ts : gestion des flux RSS et articles - TodosService.ts : gestion des tâches globales - SettingsService.ts : gestion des paramètres de l'application Composants ajoutés: - RichTextEditor.tsx : éditeur de texte enrichi avec barre d'outils Modifications techniques: - Ajout de la propriété "files" au type Note - Résolution du conflit de nommage avec Settings - Mise à jour de package.json avec la nouvelle description - Mise à jour de index.html avec favicon et titre Cette version représente une évolution majeure de l'application avec une expérience utilisateur nettement améliorée et de nombreuses fonctionnalités demandées.
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