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
+1022
-209
No files matched your search
+2
-1
@@ -2,8 +2,9 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<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" />
|
||||
<title>Notes App</title>
|
||||
<title>NoteFlow - Notes et gestion de tâches</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "noteflow",
|
||||
"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",
|
||||
"scripts": {
|
||||
"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>
|
||||
);
|
||||
}
|
||||
+613
-207
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||
|
||||
@@ -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