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:
Claude committed 2025-11-12 09:11:11 +00:00
1 parent 44e6f7bb5e
commit 702d0c2088
10 files changed
+946 -5

No files matched your search

+4 -1
View File
@@ -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
View File
@@ -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>
+62
View File
@@ -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();
+2
View File
@@ -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 {