mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-15 11:17:42 +02:00
Feature: Support multi-images, Google Calendar et correction RTL
Images multiples : - Ajout table note_images dans la base de données - Routes POST/GET/DELETE /api/notes/:id/images - Dossier uploads/images séparé pour les images - Support de plusieurs images par note Google Calendar : - Ajout table calendar_events - Routes /api/calendar pour sync et récupération des événements - Box Rendez-vous dans l'interface avec affichage des événements à venir - Configuration Google Calendar dans l'admin (API key et Calendar ID) - Synchronisation manuelle avec Google Calendar API - Installation package googleapis Corrections : - Fix RichTextEditor: ajout dir="ltr" pour empêcher l'écriture RTL - Fix Image type conflict (lucide-react vs Note interface) - Ajout original_name à l'interface Image pour compatibilité Interface : - Box Rendez-vous empilée avec Todos et RSS dans la colonne droite - Nouvel onglet Google Calendar dans l'administration - Bouton "Synchroniser" pour rafraîchir les événements
This commit is contained in:
10 files changed
+946
-5
No files matched your search
@@ -158,12 +158,15 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
|
||||
contentEditable
|
||||
onInput={updateContent}
|
||||
onKeyDown={handleKeyDown}
|
||||
dir="ltr"
|
||||
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'
|
||||
lineHeight: '1.6',
|
||||
direction: 'ltr',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
+154
-3
@@ -13,7 +13,7 @@ import { RichTextEditor } from "@/components/RichTextEditor";
|
||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||
import {
|
||||
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit
|
||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon
|
||||
} from "lucide-react";
|
||||
import AuthService from "@/services/AuthService";
|
||||
import AdminService from "@/services/AdminService";
|
||||
@@ -21,6 +21,7 @@ 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 CalendarService, { CalendarEvent } from "@/services/CalendarService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { showError, showSuccess } from "@/utils/toast";
|
||||
|
||||
@@ -40,6 +41,7 @@ const Index = () => {
|
||||
const [todos, setTodos] = useState<Todo[]>([]);
|
||||
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
|
||||
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
|
||||
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
|
||||
const [settings, setSettings] = useState<AppSettings>({});
|
||||
const [adminTab, setAdminTab] = useState("users");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
@@ -53,6 +55,7 @@ const Index = () => {
|
||||
loadTodos();
|
||||
loadRssFeeds();
|
||||
loadRssArticles();
|
||||
loadCalendarEvents();
|
||||
} else {
|
||||
navigate("/login");
|
||||
}
|
||||
@@ -102,6 +105,17 @@ const Index = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCalendarEvents = async () => {
|
||||
try {
|
||||
const events = await CalendarService.getEvents(10);
|
||||
if (Array.isArray(events)) {
|
||||
setCalendarEvents(events);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement des événements:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (user?.is_admin) {
|
||||
try {
|
||||
@@ -898,11 +912,80 @@ const Index = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Calendar Box */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<CalendarIcon className="h-6 w-6" />
|
||||
Rendez-vous
|
||||
</CardTitle>
|
||||
<Button size="lg" variant="outline" onClick={async () => {
|
||||
try {
|
||||
const result = await CalendarService.sync();
|
||||
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||
await loadCalendarEvents();
|
||||
} catch (error) {
|
||||
showError("Erreur lors de la synchronisation");
|
||||
}
|
||||
}} className="gap-2">
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
Synchroniser
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-[480px] overflow-y-auto">
|
||||
{calendarEvents.length > 0 ? (
|
||||
calendarEvents.map(event => {
|
||||
const startDate = new Date(event.start_time);
|
||||
const endDate = new Date(event.end_time);
|
||||
const isToday = startDate.toDateString() === new Date().toDateString();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium line-clamp-2 mb-1">{event.title}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR')} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
{event.location && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
📍 {event.location}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ExternalLink className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-center text-muted-foreground py-8">
|
||||
Aucun rendez-vous à venir
|
||||
<br />
|
||||
<Button
|
||||
variant="link"
|
||||
className="mt-2"
|
||||
onClick={() => setShowAdmin(true)}
|
||||
>
|
||||
Configurer Google Calendar
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin Modal */}
|
||||
{/* Admin Modal */}
|
||||
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
@@ -914,9 +997,10 @@ const Index = () => {
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={adminTab} onValueChange={setAdminTab}>
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
<TabsList className="grid grid-cols-5 w-full">
|
||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||
<TabsTrigger value="rss">Flux RSS</TabsTrigger>
|
||||
<TabsTrigger value="calendar">Google Calendar</TabsTrigger>
|
||||
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
||||
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -1003,6 +1087,73 @@ const Index = () => {
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="calendar" className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
Configuration Google Calendar
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="calendar-api-key">Clé API Google Calendar</Label>
|
||||
<Input
|
||||
id="calendar-api-key"
|
||||
type="password"
|
||||
placeholder="AIza..."
|
||||
value={settings.google_calendar_api_key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Créez une clé API sur{' '}
|
||||
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
Google Cloud Console
|
||||
</a>
|
||||
{' '}et activez l'API Google Calendar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="calendar-id">ID du calendrier</Label>
|
||||
<Input
|
||||
id="calendar-id"
|
||||
placeholder="votre-email@gmail.com ou id-calendrier"
|
||||
value={settings.google_calendar_id || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Trouvez l'ID du calendrier dans les paramètres de votre calendrier Google.<br />
|
||||
Pour votre calendrier principal, utilisez votre adresse email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
Enregistrer
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await CalendarService.sync();
|
||||
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||
await loadCalendarEvents();
|
||||
} catch (error) {
|
||||
showError("Erreur lors de la synchronisation. Vérifiez votre configuration.");
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Tester la synchronisation
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="openrouter" className="mt-4 space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import axios from "axios";
|
||||
import AuthService from "./AuthService";
|
||||
|
||||
export interface CalendarEvent {
|
||||
id: number;
|
||||
google_event_id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
location?: string;
|
||||
html_link?: string;
|
||||
synced_at?: string;
|
||||
}
|
||||
|
||||
class CalendarService {
|
||||
private baseURL = "/api/calendar";
|
||||
|
||||
private getAuthHeader() {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${AuthService.getToken()}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les événements du calendrier
|
||||
*/
|
||||
async getEvents(limit: number = 10): Promise<CalendarEvent[]> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseURL}/events?limit=${limit}`, this.getAuthHeader());
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des événements:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchroniser avec Google Calendar
|
||||
*/
|
||||
async sync(): Promise<{ syncedCount: number; events: number }> {
|
||||
const response = await axios.post(`${this.baseURL}/sync`, {}, this.getAuthHeader());
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un événement local
|
||||
*/
|
||||
async deleteEvent(eventId: number): Promise<boolean> {
|
||||
try {
|
||||
await axios.delete(`${this.baseURL}/events/${eventId}`, this.getAuthHeader());
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la suppression de l'événement:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new CalendarService();
|
||||
@@ -5,6 +5,8 @@ export interface Settings {
|
||||
ai_model?: string;
|
||||
rss_refresh_interval?: number;
|
||||
theme?: 'light' | 'dark' | 'auto';
|
||||
google_calendar_api_key?: string;
|
||||
google_calendar_id?: string;
|
||||
}
|
||||
|
||||
class SettingsService {
|
||||
|
||||
Reference in new issue
Block a user