From 23159338680de9e4ddd8abd890a9cccb904846ed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Nov 2025 17:10:22 +0000 Subject: [PATCH] =?UTF-8?q?React:=20Application=20compl=C3=A8te=20en=20Rea?= =?UTF-8?q?ct=20+=20TypeScript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture: - Types TypeScript complets pour toutes les entités - Service API avec axios et intercepteurs JWT - AuthContext pour gestion authentification - Router avec routes protégées (login, dashboard) Composants: - Login: page de connexion avec formulaire - Dashboard: page principale avec 3 colonnes (notes, todos, rss) - Affichage conditionnel articles vs résumés RSS - Gestion todos en temps réel - Design moderne et responsive Configuration: - Vite avec proxy API vers backend (:3000) - Build vers /public-react - Configuration TypeScript stricte - CSS moderne avec variables et responsive Features: - Authentication complète - Notes affichées en grille - Todos sidebar avec CRUD - RSS sidebar avec articles ou résumés - Protection des routes - UX fluide et moderne --- client/src/App.tsx | 89 +++++--- client/src/contexts/AuthContext.tsx | 66 ++++++ client/src/index.css | 95 +++------ client/src/pages/Dashboard.css | 303 ++++++++++++++++++++++++++++ client/src/pages/Dashboard.tsx | 199 ++++++++++++++++++ client/src/pages/Login.css | 120 +++++++++++ client/src/pages/Login.tsx | 77 +++++++ client/src/services/api.ts | 217 ++++++++++++++++++++ client/src/types/index.ts | 114 +++++++++++ client/vite.config.ts | 16 ++ 10 files changed, 1202 insertions(+), 94 deletions(-) create mode 100644 client/src/contexts/AuthContext.tsx create mode 100644 client/src/pages/Dashboard.css create mode 100644 client/src/pages/Dashboard.tsx create mode 100644 client/src/pages/Login.css create mode 100644 client/src/pages/Login.tsx create mode 100644 client/src/services/api.ts create mode 100644 client/src/types/index.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 3d7ded3..dc91be6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,35 +1,62 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; -function App() { - const [count, setCount] = useState(0) +// Protected Route Component +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated } = useAuth(); - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) + if (!isAuthenticated) { + return ; + } + + return <>{children}; } -export default App +// Public Route Component (redirect if already logged in) +function PublicRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated } = useAuth(); + + if (isAuthenticated) { + return ; + } + + return <>{children}; +} + +function AppRoutes() { + return ( + + + + + } + /> + + + + } + /> + } /> + + ); +} + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/client/src/contexts/AuthContext.tsx b/client/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..ae1a345 --- /dev/null +++ b/client/src/contexts/AuthContext.tsx @@ -0,0 +1,66 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { authApi } from '../services/api'; +import type { User, AuthContextType } from '../types'; + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Récupérer le token et user du localStorage au démarrage + const storedToken = localStorage.getItem('token'); + const storedUser = localStorage.getItem('user'); + + if (storedToken && storedUser) { + setToken(storedToken); + setUser(JSON.parse(storedUser)); + } + + setLoading(false); + }, []); + + const login = async (username: string, password: string) => { + const data = await authApi.login(username, password); + + setToken(data.token); + setUser(data.user); + + localStorage.setItem('token', data.token); + localStorage.setItem('user', JSON.stringify(data.user)); + }; + + const logout = () => { + setToken(null); + setUser(null); + + localStorage.removeItem('token'); + localStorage.removeItem('user'); + + authApi.logout().catch(() => {}); + }; + + const value: AuthContextType = { + user, + token, + login, + logout, + isAuthenticated: !!token, + }; + + if (loading) { + return
Chargement...
; + } + + return {children}; +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +}; diff --git a/client/src/index.css b/client/src/index.css index 08a3ac9..23f264b 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,68 +1,37 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +* { + margin: 0; + padding: 0; + box-sizing: border-box; } body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + color: #1a202c; + background: #f7fafc; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +:root { + --color-primary: #667eea; + --color-secondary: #764ba2; + --color-text: #1a202c; + --color-text-light: #718096; + --color-border: #e2e8f0; + --color-bg: #f7fafc; + --color-card: #ffffff; + --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +#root { min-height: 100vh; } - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/client/src/pages/Dashboard.css b/client/src/pages/Dashboard.css new file mode 100644 index 0000000..149ec32 --- /dev/null +++ b/client/src/pages/Dashboard.css @@ -0,0 +1,303 @@ +.dashboard { + min-height: 100vh; + background: #f7fafc; +} + +.dashboard-header { + background: white; + border-bottom: 1px solid #e2e8f0; + padding: 16px 24px; + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.header-left h1 { + margin: 0; + font-size: 24px; + color: #1a202c; +} + +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + +.user-name { + color: #4a5568; + font-weight: 500; +} + +.btn-logout { + padding: 8px 16px; + background: #e2e8f0; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.btn-logout:hover { + background: #cbd5e0; +} + +.dashboard-main { + padding: 24px; + max-width: 1800px; + margin: 0 auto; +} + +.content-grid { + display: grid; + grid-template-columns: 1fr 320px 320px; + gap: 24px; + align-items: start; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.section-header h2 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: #1a202c; +} + +.btn-primary { + padding: 8px 16px; + background: #667eea; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 500; + transition: all 0.2s; +} + +.btn-primary:hover { + background: #5a67d8; + transform: translateY(-1px); +} + +/* Notes Section */ +.notes-section { + background: transparent; +} + +.notes-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.note-card { + background: white; + padding: 20px; + border-radius: 12px; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.2s; +} + +.note-card:hover { + border-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); + transform: translateY(-2px); +} + +.note-card h3 { + margin: 0 0 12px 0; + font-size: 16px; + font-weight: 600; + color: #1a202c; +} + +.note-card p { + margin: 0; + color: #718096; + font-size: 14px; + line-height: 1.5; +} + +.note-todos-preview { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #e2e8f0; + font-size: 13px; + color: #667eea; + font-weight: 500; +} + +/* Todos Sidebar */ +.todos-sidebar, +.rss-sidebar { + background: white; + border-radius: 12px; + border: 1px solid #e2e8f0; + padding: 20px; + position: sticky; + top: 88px; + max-height: calc(100vh - 112px); + overflow-y: auto; +} + +.todo-input-container { + margin-bottom: 16px; +} + +.todo-input-container input { + width: 100%; + padding: 10px 12px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 14px; + transition: border-color 0.2s; +} + +.todo-input-container input:focus { + outline: none; + border-color: #667eea; +} + +.todos-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.todo-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px; + background: #f7fafc; + border-radius: 6px; + transition: background 0.2s; +} + +.todo-item:hover { + background: #edf2f7; +} + +.todo-item input[type="checkbox"] { + cursor: pointer; +} + +.todo-item span { + flex: 1; + font-size: 14px; + color: #2d3748; +} + +.todo-item span.completed { + text-decoration: line-through; + color: #a0aec0; +} + +.btn-delete { + padding: 4px 8px; + background: transparent; + border: none; + color: #cbd5e0; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s; +} + +.btn-delete:hover { + background: #fed7d7; + color: #c53030; +} + +/* RSS Sidebar */ +.rss-content { + display: flex; + flex-direction: column; + gap: 12px; +} + +.rss-article, +.rss-summary { + padding: 12px; + background: #f7fafc; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + border: 1px solid transparent; +} + +.rss-article:hover { + background: #edf2f7; + border-color: #667eea; +} + +.rss-article h4, +.rss-summary h4 { + margin: 0 0 8px 0; + font-size: 14px; + font-weight: 600; + color: #1a202c; + line-height: 1.4; +} + +.rss-article p, +.rss-summary p { + margin: 8px 0 0 0; + font-size: 13px; + color: #718096; + line-height: 1.5; +} + +.article-meta, +.summary-meta { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #a0aec0; + margin-top: 8px; +} + +.empty-state { + text-align: center; + color: #a0aec0; + font-size: 14px; + padding: 40px 20px; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + font-size: 18px; + color: #667eea; +} + +/* Responsive */ +@media (max-width: 1400px) { + .content-grid { + grid-template-columns: 1fr 280px 280px; + } +} + +@media (max-width: 1200px) { + .content-grid { + grid-template-columns: 1fr; + } + + .todos-sidebar, + .rss-sidebar { + position: static; + max-height: none; + } +} diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx new file mode 100644 index 0000000..a59515b --- /dev/null +++ b/client/src/pages/Dashboard.tsx @@ -0,0 +1,199 @@ +import { useState, useEffect } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { notesApi, todosApi, rssApi, settingsApi } from '../services/api'; +import type { Note, GlobalTodo, RssArticle, RssSummary } from '../types'; +import './Dashboard.css'; + +export default function Dashboard() { + const { user, logout } = useAuth(); + const [notes, setNotes] = useState([]); + const [todos, setTodos] = useState([]); + const [rssContent, setRssContent] = useState([]); + const [showSummaries, setShowSummaries] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const [notesData, todosData, settings] = await Promise.all([ + notesApi.getAll(false), + todosApi.getAll(), + settingsApi.getAll(), + ]); + + setNotes(notesData); + setTodos(todosData); + + const summaryEnabled = settings.rss_summary_enabled === '1'; + setShowSummaries(summaryEnabled); + + if (summaryEnabled) { + const summaries = await rssApi.getSummaries(); + setRssContent(summaries); + } else { + const articles = await rssApi.getArticles(); + setRssContent(articles); + } + } catch (error) { + console.error('Erreur chargement données:', error); + } finally { + setLoading(false); + } + }; + + const addTodo = async (text: string) => { + if (!text.trim()) return; + try { + const newTodo = await todosApi.create(text); + setTodos([...todos, newTodo]); + } catch (error) { + console.error('Erreur ajout todo:', error); + } + }; + + const toggleTodo = async (id: number, completed: boolean) => { + try { + await todosApi.update(id, completed); + setTodos(todos.map(t => t.id === id ? { ...t, completed } : t)); + } catch (error) { + console.error('Erreur toggle todo:', error); + } + }; + + const deleteTodo = async (id: number) => { + if (!window.confirm('Supprimer cette tâche ?')) return; + try { + await todosApi.delete(id); + setTodos(todos.filter(t => t.id !== id)); + } catch (error) { + console.error('Erreur suppression todo:', error); + } + }; + + if (loading) { + return
Chargement...
; + } + + return ( +
+ {/* Header */} +
+
+

📝 NoteFlow

+
+
+ {user?.username} + +
+
+ + {/* Main Content */} +
+
+ {/* Notes Section */} +
+
+

Notes

+ +
+
+ {notes.length === 0 ? ( +

Aucune note

+ ) : ( + notes.map(note => ( +
+

{note.title}

+ {note.content &&

{note.content.substring(0, 150)}...

} + {note.todos && note.todos.length > 0 && ( +
+ {note.todos_completed}/{note.todos_count} tâches +
+ )} +
+ )) + )} +
+
+ + {/* Todos Sidebar */} + + + {/* RSS Sidebar */} + +
+
+
+ ); +} diff --git a/client/src/pages/Login.css b/client/src/pages/Login.css new file mode 100644 index 0000000..cc11a73 --- /dev/null +++ b/client/src/pages/Login.css @@ -0,0 +1,120 @@ +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; +} + +.login-container { + background: white; + border-radius: 16px; + padding: 40px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 420px; + width: 100%; +} + +.login-header { + text-align: center; + margin-bottom: 32px; +} + +.login-header h1 { + font-size: 32px; + margin: 0 0 8px 0; + color: #1a202c; +} + +.login-header p { + margin: 0; + color: #718096; + font-size: 14px; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.form-group label { + font-size: 14px; + font-weight: 600; + color: #2d3748; +} + +.form-group input { + padding: 12px 16px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 15px; + transition: all 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.error-message { + background: #fed7d7; + color: #c53030; + padding: 12px 16px; + border-radius: 8px; + font-size: 14px; + border: 1px solid #fc8181; +} + +.btn-login { + padding: 14px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + margin-top: 8px; +} + +.btn-login:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); +} + +.btn-login:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.login-footer { + text-align: center; + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid #e2e8f0; +} + +.login-footer p { + margin: 0; + font-size: 14px; + color: #718096; +} + +.login-footer strong { + color: #2d3748; +} diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx new file mode 100644 index 0000000..d4ab6f9 --- /dev/null +++ b/client/src/pages/Login.tsx @@ -0,0 +1,77 @@ +import { useState, FormEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import './Login.css'; + +export default function Login() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const { login } = useAuth(); + const navigate = useNavigate(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + await login(username, password); + navigate('/'); + } catch (err: any) { + setError(err.response?.data?.error || 'Erreur de connexion'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

📝 NoteFlow

+

Connectez-vous pour accéder à vos notes

+
+ +
+ {error &&
{error}
} + +
+ + setUsername(e.target.value)} + required + autoFocus + disabled={loading} + /> +
+ +
+ + setPassword(e.target.value)} + required + disabled={loading} + /> +
+ + +
+ +
+

Compte par défaut : admin / admin

+
+
+
+ ); +} diff --git a/client/src/services/api.ts b/client/src/services/api.ts new file mode 100644 index 0000000..9d13939 --- /dev/null +++ b/client/src/services/api.ts @@ -0,0 +1,217 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + User, + Note, + Todo, + GlobalTodo, + RssFeed, + RssArticle, + RssSummary, + OpenRouterModel, + Settings, + NoteFile +} from '../types'; + +// Configuration Axios +const api: AxiosInstance = axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Intercepteur pour ajouter le token JWT +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Intercepteur pour gérer les erreurs d'authentification +api.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response?.status === 401 || error.response?.status === 403) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +// === AUTH === +export const authApi = { + login: async (username: string, password: string) => { + const { data } = await api.post('/auth/login', { username, password }); + return data; + }, + register: async (username: string, password: string) => { + const { data } = await api.post('/auth/register', { username, password }); + return data; + }, + logout: async () => { + const { data } = await api.post('/auth/logout'); + return data; + }, +}; + +// === USERS === +export const usersApi = { + getAll: async (): Promise => { + const { data } = await api.get('/users'); + return data; + }, + create: async (username: string, password: string, is_admin: boolean): Promise => { + const { data } = await api.post('/users', { username, password, is_admin }); + return data; + }, + update: async (id: number, password: string): Promise => { + await api.put(`/users/${id}`, { password }); + }, + delete: async (id: number): Promise => { + await api.delete(`/users/${id}`); + }, +}; + +// === NOTES === +export const notesApi = { + getAll: async (archived: boolean = false): Promise => { + const { data } = await api.get(`/notes?archived=${archived}`); + return Array.isArray(data) ? data : []; + }, + getOne: async (id: number): Promise => { + const { data } = await api.get(`/notes/${id}`); + return data; + }, + create: async (note: Partial): Promise => { + const { data } = await api.post('/notes', note); + return data; + }, + update: async (id: number, note: Partial): Promise => { + await api.put(`/notes/${id}`, note); + }, + delete: async (id: number): Promise => { + await api.delete(`/notes/${id}`); + }, + archive: async (id: number, archived: boolean): Promise => { + await api.put(`/notes/${id}/archive`, { archived }); + }, + uploadImage: async (id: number, file: File): Promise<{ filename: string; url: string }> => { + const formData = new FormData(); + formData.append('image', file); + const { data } = await api.post(`/notes/${id}/image`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return data; + }, + deleteImage: async (id: number): Promise => { + await api.delete(`/notes/${id}/image`); + }, + uploadFile: async (id: number, file: File): Promise => { + const formData = new FormData(); + formData.append('file', file); + const { data } = await api.post(`/notes/${id}/files`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return data; + }, + downloadFile: async (fileId: number): Promise => { + window.open(`/api/notes/files/${fileId}/download`, '_blank'); + }, + deleteFile: async (fileId: number): Promise => { + await api.delete(`/api/notes/files/${fileId}`); + }, +}; + +// === NOTE TODOS === +export const noteTodosApi = { + create: async (noteId: number, text: string): Promise => { + const { data } = await api.post(`/notes/${noteId}/todos`, { text }); + return data; + }, + update: async (id: number, completed: boolean): Promise => { + await api.put(`/notes/todos/${id}`, { completed }); + }, + delete: async (id: number): Promise => { + await api.delete(`/notes/todos/${id}`); + }, +}; + +// === GLOBAL TODOS === +export const todosApi = { + getAll: async (): Promise => { + const { data} = await api.get('/todos'); + return data; + }, + create: async (text: string): Promise => { + const { data } = await api.post('/todos', { text }); + return data; + }, + update: async (id: number, completed: boolean): Promise => { + await api.put(`/todos/${id}`, { completed }); + }, + delete: async (id: number): Promise => { + await api.delete(`/todos/${id}`); + }, +}; + +// === RSS FEEDS === +export const rssApi = { + getFeeds: async (): Promise => { + const { data } = await api.get('/rss/feeds'); + return data; + }, + addFeed: async (url: string): Promise => { + const { data } = await api.post('/rss/feeds', { url }); + return data; + }, + updateFeed: async (id: number, enabled: boolean): Promise => { + await api.put(`/rss/feeds/${id}`, { enabled }); + }, + deleteFeed: async (id: number): Promise => { + await api.delete(`/rss/feeds/${id}`); + }, + fetchArticles: async (): Promise<{ message: string }> => { + const { data } = await api.post('/rss/fetch'); + return data; + }, + getArticles: async (): Promise => { + const { data } = await api.get('/rss/articles'); + return data || []; + }, + getSummaries: async (): Promise => { + const { data } = await api.get('/rss/summaries'); + return data || []; + }, + generateSummary: async (): Promise<{ summary: string; model: string; articles_count: number }> => { + const { data } = await api.post('/rss/summarize'); + return data; + }, + getModels: async (): Promise => { + const { data } = await api.get('/rss/models'); + return data; + }, +}; + +// === SETTINGS === +export const settingsApi = { + getAll: async (): Promise => { + const { data } = await api.get('/settings'); + return data; + }, + getOne: async (key: string): Promise<{ key: string; value: string }> => { + const { data } = await api.get(`/settings/${key}`); + return data; + }, + update: async (key: string, value: string): Promise => { + await api.put(`/settings/${key}`, { value }); + }, + delete: async (key: string): Promise => { + await api.delete(`/settings/${key}`); + }, +}; + +export default api; diff --git a/client/src/types/index.ts b/client/src/types/index.ts new file mode 100644 index 0000000..961c85a --- /dev/null +++ b/client/src/types/index.ts @@ -0,0 +1,114 @@ +// Types pour l'application NoteFlow + +export interface User { + id: number; + username: string; + is_admin: boolean; + created_at: string; +} + +export interface Note { + id: number; + user_id: number; + title: string; + content: string; + image_filename?: string; + archived: boolean; + created_at: string; + updated_at: string; + todos?: Todo[]; + files?: NoteFile[]; + todos_count?: number; + todos_completed?: number; +} + +export interface Todo { + id: number; + note_id: number; + text: string; + completed: boolean; + position: number; +} + +export interface GlobalTodo { + id: number; + user_id: number; + text: string; + completed: boolean; + created_at: string; +} + +export interface NoteFile { + id: number; + note_id: number; + filename: string; + original_name: string; + file_size: number; + mime_type: string; + created_at: string; +} + +export interface RssFeed { + id: number; + url: string; + title: string; + description: string; + enabled: boolean; + created_at: string; + last_fetched_at?: string; +} + +export interface RssArticle { + id: number; + feed_id: number; + title: string; + link: string; + description?: string; + pub_date: string; + content?: string; + feed_title: string; + feed_url: string; +} + +export interface RssSummary { + id: number; + summary: string; + model: string; + articles_count: number; + created_at: string; +} + +export interface OpenRouterModel { + id: string; + name: string; + provider: string; + context_length?: number; + pricing?: { + prompt: number; + completion: number; + }; +} + +export interface Settings { + [key: string]: string; + openrouter_api_key?: string; + openrouter_model?: string; + rss_summary_enabled?: string; + rss_summary_prompt?: string; +} + +export interface AuthContextType { + user: User | null; + token: string | null; + login: (username: string, password: string) => Promise; + logout: () => void; + isAuthenticated: boolean; +} + +export interface ApiError { + error: string; + details?: any; +} + +export type NoteView = 'active' | 'archived'; +export type TodoFilter = 'all' | 'active' | 'completed'; diff --git a/client/vite.config.ts b/client/vite.config.ts index 8b0f57b..32b7bcb 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -4,4 +4,20 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/uploads': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, + build: { + outDir: '../public-react', + }, })