- {/* Top Navigation */}
-
-
-
-
-
-
- {user && (
-
-
-
-
-
- Mon compte
-
- {user.is_admin && (
- setShowAdmin(true)}>
-
- Administration
-
- )}
-
-
- Déconnexion
-
-
-
- )}
-
-
-
-
-
- {/* Main Content */}
-
-
- {/* Sidebar */}
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
-
-
-
- Toutes
- Archivées
-
-
-
-
- {filteredNotes.length === 0 ? (
-
Aucune note trouvée
- ) : (
- filteredNotes.map(note => (
-
setCurrentNote(note)}
- >
-
- {note.title || "Sans titre"}
-
- {note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
-
-
-
- ))
- )}
-
-
-
- {/* Note Editor */}
- {currentNote ? (
-
-
-
handleUpdateNote({ title: e.target.value })}
- />
-
-
-
-
-
-
-
-
-
- Contenu
- Tâches
- Images
-
-
-
-
-
- handleUpdateNote({ content: e.currentTarget.innerHTML })}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {currentNote.todos && currentNote.todos.length > 0 ? (
- currentNote.todos.map((todo, index) => (
-
- {
- const updatedTodos = [...(currentNote.todos || [])];
- updatedTodos[index].completed = !updatedTodos[index].completed;
- handleUpdateNote({ todos: updatedTodos });
- }}
- className="h-4 w-4"
- />
-
- {todo.text}
-
-
-
- ))
- ) : (
-
Aucune tâche
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {currentNote.images && currentNote.images.length > 0 ? (
- currentNote.images.map((image, index) => (
-
-

-
-
- ))
- ) : (
-
Aucune image
- )}
-
-
-
-
-
-
- ) : (
-
-
-
Aucune note sélectionnée
-
-
-
- )}
-
-
-
- {/* Admin Modal */}
-
-
-
-
-
-
- );
-};
-
-export default Index;
\ No newline at end of file
+import { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
+import { Avatar, AvatarFallback } from "@/components/ui/avatar";
+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 {
+ 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 AdminService from "@/services/AdminService";
+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 { showError, showSuccess } from "@/utils/toast";
+
+interface UserType {
+ id: number;
+ username: string;
+ is_admin: boolean;
+}
+
+const Index = () => {
+ const [user, setUser] = useState
(null);
+ const [showAdmin, setShowAdmin] = useState(false);
+ const [activeTab, setActiveTab] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [notes, setNotes] = useState([]);
+ const [currentNote, setCurrentNote] = useState(null);
+ const [users, setUsers] = useState([]);
+ const [todos, setTodos] = useState([]);
+ const [rssFeeds, setRssFeeds] = useState([]);
+ const [rssArticles, setRssArticles] = useState([]);
+ const [settings, setSettings] = useState({});
+ const [adminTab, setAdminTab] = useState("users");
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const userFromAuth = AuthService.getUser();
+ if (userFromAuth) {
+ setUser(userFromAuth);
+ loadNotes();
+ loadTodos();
+ loadRssFeeds();
+ loadRssArticles();
+ } else {
+ navigate("/login");
+ }
+ }, [navigate]);
+
+ const loadNotes = async () => {
+ try {
+ const fetchedNotes = await NotesService.getNotes();
+ if (Array.isArray(fetchedNotes)) {
+ setNotes(fetchedNotes);
+ if (fetchedNotes.length > 0) {
+ setCurrentNote(fetchedNotes[0]);
+ }
+ }
+ } catch (error) {
+ console.error("Erreur lors du chargement des notes:", 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);
+ }
+ };
+
+ const loadUsers = async () => {
+ if (user?.is_admin) {
+ try {
+ const fetchedUsers = await AdminService.getUsers();
+ if (Array.isArray(fetchedUsers)) {
+ setUsers(fetchedUsers);
+ }
+ } catch (error) {
+ console.error("Erreur lors du chargement des utilisateurs:", error);
+ }
+ }
+ };
+
+ 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 = () => {
+ AuthService.logout();
+ navigate("/login");
+ };
+
+ const handleCreateNote = async () => {
+ try {
+ const newNote = await NotesService.createNote("Nouvelle note");
+ if (newNote) {
+ setNotes([newNote, ...notes]);
+ setCurrentNote(newNote);
+ }
+ } catch (error) {
+ showError("Erreur lors de la création de la note");
+ }
+ };
+
+ const handleUpdateNote = async (updatedFields: Partial) => {
+ if (!currentNote) return;
+
+ try {
+ const updatedNote = {
+ ...currentNote,
+ ...updatedFields,
+ todos: currentNote.todos || [],
+ images: currentNote.images || [],
+ files: currentNote.files || []
+ };
+
+ const success = await NotesService.updateNote(updatedNote);
+
+ if (success) {
+ setCurrentNote(updatedNote);
+ setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note));
+ }
+ } catch (error) {
+ showError("Erreur lors de la mise à jour de la note");
+ }
+ };
+
+ const handleDeleteNote = async () => {
+ if (!currentNote?.id) return;
+
+ if (window.confirm("Êtes-vous sûr de vouloir supprimer cette note ?")) {
+ try {
+ const success = await NotesService.deleteNote(currentNote.id);
+
+ if (success) {
+ const updatedNotes = notes.filter(note => note.id !== currentNote.id);
+ setNotes(updatedNotes);
+ setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null);
+ showSuccess("Note supprimée");
+ }
+ } catch (error) {
+ showError("Erreur lors de la suppression");
+ }
+ }
+ };
+
+ const handleAddTodo = async () => {
+ const text = prompt("Nouvelle tâche:");
+ if (text) {
+ try {
+ const newTodo = await TodosService.createTodo(text);
+ if (newTodo) {
+ setTodos([newTodo, ...todos]);
+ showSuccess("Tâche ajoutée");
+ }
+ } catch (error) {
+ showError("Erreur lors de l'ajout de la tâche");
+ }
+ }
+ };
+
+ const 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 username = prompt("Nom d'utilisateur:");
+ if (!username) return;
+ const password = prompt("Mot de passe:");
+ if (!password) return;
+ const isAdmin = window.confirm("Donner les droits administrateur ?");
+
+ try {
+ const newUser = await AdminService.createUser(username, password, isAdmin);
+ if (newUser) {
+ loadUsers();
+ showSuccess("Utilisateur créé");
+ }
+ } catch (error) {
+ showError("Erreur lors de la création");
+ }
+ };
+
+ const handleDeleteUser = async (userId: number) => {
+ if (window.confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) {
+ try {
+ const success = await AdminService.deleteUser(userId);
+ if (success) {
+ loadUsers();
+ showSuccess("Utilisateur supprimé");
+ }
+ } catch (error) {
+ showError("Erreur lors de la suppression");
+ }
+ }
+ };
+
+ const handleSaveSettings = async () => {
+ try {
+ await SettingsService.updateSettings(settings);
+ showSuccess("Paramètres enregistrés");
+ } catch (error) {
+ showError("Erreur lors de l'enregistrement");
+ }
+ };
+
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ 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 =>
+ (activeTab === "all" || (activeTab === "archived" && note.archived)) &&
+ (searchQuery === "" ||
+ (note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
+ (note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase())))
+ );
+
+ return (
+
+ {/* Top Navigation */}
+
+
+
+
+
+
+ {user && (
+
+
+
+
+
+ Mon compte
+
+ {user.is_admin && (
+ setShowAdmin(true)}>
+
+ Administration
+
+ )}
+
+
+ Déconnexion
+
+
+
+ )}
+
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Left Sidebar - Notes */}
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+ Toutes
+ Archivées
+
+
+
+
+ {filteredNotes.length === 0 ? (
+
Aucune note trouvée
+ ) : (
+ filteredNotes.map(note => (
+
setCurrentNote(note)}
+ >
+
+ {note.title || "Sans titre"}
+
+ {note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
+
+
+ {note.todos && note.todos.length > 0 && (
+
+
+ {note.todos.length}
+
+ )}
+ {note.images && note.images.length > 0 && (
+
+
+ {note.images.length}
+
+ )}
+ {note.files && note.files.length > 0 && (
+
+
+ {note.files.length}
+
+ )}
+
+
+
+ ))
+ )}
+
+
+
+ {/* Center - Note Editor */}
+ {currentNote ? (
+
+
+
handleUpdateNote({ title: e.target.value })}
+ />
+
+
+
+
+
+
+
+
+
+ Contenu
+
+ Tâches
+ {currentNote.todos && currentNote.todos.length > 0 && (
+ {currentNote.todos.length}
+ )}
+
+
+ Images
+ {currentNote.images && currentNote.images.length > 0 && (
+ {currentNote.images.length}
+ )}
+
+
+ Fichiers
+ {currentNote.files && currentNote.files.length > 0 && (
+ {currentNote.files.length}
+ )}
+
+
+
+
+ handleUpdateNote({ content })}
+ placeholder="Commencez à écrire votre note..."
+ />
+
+
+
+
+
+
+
+
+ {currentNote.todos && currentNote.todos.length > 0 ? (
+ currentNote.todos.map((todo, index) => (
+
+ {
+ const updatedTodos = [...(currentNote.todos || [])];
+ updatedTodos[index].completed = !updatedTodos[index].completed;
+ handleUpdateNote({ todos: updatedTodos });
+ }}
+ />
+
+ {todo.text}
+
+
+
+ ))
+ ) : (
+
Aucune tâche dans cette note
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {currentNote.images && currentNote.images.length > 0 ? (
+ currentNote.images.map((image, index) => (
+
+

+
+
+ ))
+ ) : (
+
Aucune image
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {currentNote.files && currentNote.files.length > 0 ? (
+ currentNote.files.map((file: any, index: number) => (
+
+
+
+
{file.filename || file.original_name}
+
+ {file.size ? `${(file.size / 1024).toFixed(1)} KB` : ''}
+
+
+
+
+
+
+
+ ))
+ ) : (
+
Aucun fichier attaché
+ )}
+
+
+
+
+
+
+ ) : (
+
+
+
+
Aucune note sélectionnée
+
+
+
+ )}
+
+ {/* Right Sidebar - Todos & RSS */}
+
+ {/* Todos Box */}
+
+
+
+
+
+
+ {todos.length > 0 ? (
+ todos.map(todo => (
+
+ todo.id && handleToggleTodo(todo.id)}
+ />
+
+ {todo.text}
+
+
+
+ ))
+ ) : (
+
Aucune tâche
+ )}
+
+
+
+
+ {/* RSS Box */}
+
+
+
+
+
+
+ {rssArticles.length > 0 ? (
+ rssArticles.map(article => (
+
article.link && window.open(article.link, '_blank')}
+ >
+
+
+
{article.title}
+ {article.description && (
+
+ {article.description.replace(/<[^>]*>/g, '')}
+
+ )}
+
+
+
+
+ ))
+ ) : (
+
+ Aucun article
+
+
+
+ )}
+
+
+
+
+
+
+
+ {/* Admin Modal */}
+
+
+
+
+
+
+ );
+};
+
+export default Index;
diff --git a/src/services/NotesService.ts b/src/services/NotesService.ts
index aa5fe61..cbfa60e 100644
--- a/src/services/NotesService.ts
+++ b/src/services/NotesService.ts
@@ -13,6 +13,13 @@ interface Image {
filename: string;
}
+interface NoteFile {
+ id?: number;
+ filename: string;
+ original_name?: string;
+ size?: number;
+}
+
export interface Note {
id?: number;
title: string;
@@ -22,6 +29,7 @@ export interface Note {
updated_at?: string;
todos: Todo[];
images: Image[];
+ files?: NoteFile[];
}
class NotesService {
diff --git a/src/services/RssService.ts b/src/services/RssService.ts
new file mode 100644
index 0000000..1a61e06
--- /dev/null
+++ b/src/services/RssService.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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();
diff --git a/src/services/SettingsService.ts b/src/services/SettingsService.ts
new file mode 100644
index 0000000..dd27e22
--- /dev/null
+++ b/src/services/SettingsService.ts
@@ -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 {
+ 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 {
+ try {
+ await axios.put(this.baseURL, settings, this.getAuthHeader());
+ return true;
+ } catch (error) {
+ console.error('Error updating settings:', error);
+ throw error;
+ }
+ }
+
+ async testOpenRouterConnection(): Promise {
+ 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();
diff --git a/src/services/TodosService.ts b/src/services/TodosService.ts
new file mode 100644
index 0000000..cac7ea5
--- /dev/null
+++ b/src/services/TodosService.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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();