mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-22 22:58:15 +02:00
Merge pull request #28 from R0m1k3/claude/fix-npm-ci-build-error-011CV3c2K4ETHdBRQUs7tkJf
Feature: Implémentation complète de NoteFlow avec toutes les fonction…
This commit is contained in:
8 files changed
+1022
-209
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+613
-207
File diff suppressed because it is too large.
Load diff
@@ -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