Merge pull request #49 from R0m1k3/claude/integrate-google-calendar-api-011CV44Nie9HB7U35ArSUnNa

Claude/integrate google calendar api 011 cv44 nie9 hb7 u35 ar s un na
This commit is contained in:
LogiFlow authored and GitHub committed 2025-11-12 15:41:39 +01:00
commit 91e3764b5c
1 file changed
+214 -196
+214 -196
View File
@@ -77,6 +77,7 @@ const Index = () => {
const [changePasswordModal, setChangePasswordModal] = useState<{open: boolean, userId?: number}>({open: false}); const [changePasswordModal, setChangePasswordModal] = useState<{open: boolean, userId?: number}>({open: false});
const [addNoteTodoModal, setAddNoteTodoModal] = useState(false); const [addNoteTodoModal, setAddNoteTodoModal] = useState(false);
const [addTagModal, setAddTagModal] = useState(false); const [addTagModal, setAddTagModal] = useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
// Debounce timer for auto-save // Debounce timer for auto-save
const saveTimerRef = useRef<NodeJS.Timeout | null>(null); const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
@@ -168,7 +169,7 @@ const Index = () => {
const loadCalendarEvents = async () => { const loadCalendarEvents = async () => {
try { try {
const events = await CalendarService.getEvents(10); const events = await CalendarService.getEvents(10); // Limité à 10 événements
if (Array.isArray(events)) { if (Array.isArray(events)) {
setCalendarEvents(events); setCalendarEvents(events);
} }
@@ -618,34 +619,33 @@ const Index = () => {
)} )}
<div className="max-w-[1920px] mx-auto px-8"> <div className="max-w-[1920px] mx-auto px-8">
<div className="flex h-16 items-center justify-between"> <div className="grid grid-cols-3 h-16 items-center">
<div className="flex items-center gap-6"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center"> <FileText className="h-5 w-5 text-primary" />
<FileText className="h-5 w-5 text-primary" />
</div>
<span className="text-2xl font-semibold">NoteFlow</span>
</div> </div>
<div className="text-sm text-muted-foreground"> <span className="text-2xl font-semibold">NoteFlow</span>
<div className="font-medium"> </div>
{currentDateTime.toLocaleDateString('fr-FR', {
weekday: 'long', <div className="flex flex-col items-center text-center text-sm text-muted-foreground">
year: 'numeric', <div className="font-medium">
month: 'long', {currentDateTime.toLocaleDateString('fr-FR', {
day: 'numeric' weekday: 'long',
})} year: 'numeric',
</div> month: 'long',
<div className="text-lg font-mono"> day: 'numeric'
{currentDateTime.toLocaleTimeString('fr-FR', { })}
hour: '2-digit', </div>
minute: '2-digit', <div className="text-lg font-mono">
second: '2-digit' {currentDateTime.toLocaleTimeString('fr-FR', {
})} hour: '2-digit',
</div> minute: '2-digit',
second: '2-digit'
})}
</div> </div>
</div> </div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6 justify-end">
<Button onClick={handleCreateNote} size="lg" className="gap-2"> <Button onClick={handleCreateNote} size="lg" className="gap-2">
<PlusCircle className="h-5 w-5" /> <PlusCircle className="h-5 w-5" />
Nouvelle Note Nouvelle Note
@@ -905,56 +905,104 @@ const Index = () => {
onChange={(e) => handleUpdateNote({ title: e.target.value })} onChange={(e) => handleUpdateNote({ title: e.target.value })}
/> />
<Tabs defaultValue="content" className="mt-4"> {/* Action buttons */}
<TabsList className="grid grid-cols-5 w-full max-w-2xl"> <div className="flex gap-2 flex-wrap">
<TabsTrigger value="content">Contenu</TabsTrigger> <Button
<TabsTrigger value="todos"> variant="outline"
Tâches size="sm"
{openNote.todos && openNote.todos.length > 0 && ( onClick={() => setAddNoteTodoModal(true)}
<Badge variant="secondary" className="ml-2">{openNote.todos.length}</Badge> className="gap-2"
)} >
</TabsTrigger> <CheckSquare className="h-4 w-4" />
<TabsTrigger value="images"> Ajouter une tâche
Images </Button>
{openNote.images && openNote.images.length > 0 && (
<Badge variant="secondary" className="ml-2">{openNote.images.length}</Badge>
)}
</TabsTrigger>
<TabsTrigger value="files">
Fichiers
{openNote.files && openNote.files.length > 0 && (
<Badge variant="secondary" className="ml-2">{openNote.files.length}</Badge>
)}
</TabsTrigger>
<TabsTrigger value="tags">
Tags
{noteTags.length > 0 && (
<Badge variant="secondary" className="ml-2">{noteTags.length}</Badge>
)}
</TabsTrigger>
</TabsList>
<TabsContent value="content" className="mt-4"> <Button
<RichTextEditor variant="outline"
key={openNote.id} size="sm"
content={openNote.content || ""} onClick={() => document.getElementById('image-upload')?.click()}
onChange={handleContentChange} className="gap-2"
/> >
</TabsContent> <ImageIcon className="h-4 w-4" />
Ajouter une image
</Button>
<input
id="image-upload"
type="file"
accept="image/*"
className="hidden"
onChange={async (e) => {
if (e.target.files && e.target.files[0] && openNote?.id) {
try {
const image = await NotesService.uploadImage(openNote.id, e.target.files[0]);
if (image) {
const updatedImages = [...(openNote.images || []), image];
setOpenNote({ ...openNote, images: updatedImages });
await handleUpdateNote({ images: updatedImages });
showSuccess("Image ajoutée");
}
} catch (error) {
showError("Erreur lors de l'upload de l'image");
}
e.target.value = '';
}
}}
/>
<TabsContent value="todos" className="mt-4 space-y-4"> <Button
<Button variant="outline"
onClick={() => setAddNoteTodoModal(true)} size="sm"
className="w-full" onClick={() => document.getElementById('file-upload')?.click()}
> className="gap-2"
<Plus className="h-4 w-4 mr-2" /> >
Ajouter une tâche <Paperclip className="h-4 w-4" />
</Button> Ajouter un fichier
</Button>
<input
id="file-upload"
type="file"
className="hidden"
onChange={handleFileUpload}
/>
<div className="space-y-2"> <Button
{openNote.todos && openNote.todos.length > 0 ? ( variant="outline"
openNote.todos.map((todo, index) => ( size="sm"
<div key={index} className="flex items-center gap-3 p-3 border rounded-lg"> onClick={() => setAddTagModal(true)}
className="gap-2"
>
<TagIcon className="h-4 w-4" />
Ajouter un tag
</Button>
</div>
{/* Tags */}
{noteTags.length > 0 && (
<div className="flex gap-2 flex-wrap">
{noteTags.map((tag) => (
<Badge
key={tag.id}
variant="secondary"
className="text-sm px-2 py-1 gap-1 cursor-pointer hover:bg-destructive hover:text-destructive-foreground transition-colors group"
>
<TagIcon className="h-3 w-3" />
{tag.tag}
<X
className="h-3 w-3 opacity-50 group-hover:opacity-100"
onClick={() => tag.id && handleDeleteTag(tag.id)}
/>
</Badge>
))}
</div>
)}
{/* Todos */}
{openNote.todos && openNote.todos.length > 0 && (
<Card>
<CardContent className="p-4">
<div className="space-y-2">
{openNote.todos.map((todo, index) => (
<div key={index} className="flex items-center gap-3 p-2 border rounded">
<Checkbox <Checkbox
checked={todo.completed} checked={todo.completed}
onCheckedChange={async () => { onCheckedChange={async () => {
@@ -963,123 +1011,94 @@ const Index = () => {
await handleUpdateNote({ todos: updatedTodos }); await handleUpdateNote({ todos: updatedTodos });
}} }}
/> />
<span className={todo.completed ? "line-through text-muted-foreground flex-1" : "flex-1"}> <span className={todo.completed ? "line-through text-muted-foreground flex-1 text-sm" : "flex-1 text-sm"}>
{todo.text} {todo.text}
</span> </span>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6"
onClick={async () => { onClick={async () => {
const updatedTodos = [...(openNote.todos || [])]; const updatedTodos = [...(openNote.todos || [])];
updatedTodos.splice(index, 1); updatedTodos.splice(index, 1);
await handleUpdateNote({ todos: updatedTodos }); await handleUpdateNote({ todos: updatedTodos });
}} }}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
)) ))}
) : ( </div>
<p className="text-center text-muted-foreground py-8">Aucune tâche</p> </CardContent>
)} </Card>
</div> )}
</TabsContent>
<TabsContent value="images" className="mt-4 space-y-4"> {/* Images in thumbnails */}
<div> {openNote.images && openNote.images.length > 0 && (
<Input <div className="grid grid-cols-4 gap-3">
type="file" {openNote.images.map((image, index) => (
accept="image/*" <div key={index} className="relative group">
onChange={async (e) => { <img
if (e.target.files && e.target.files[0] && openNote.id) { src={`/uploads/images/${image.filename}`}
try { alt={image.original_name}
const image = await NotesService.uploadImage(openNote.id, e.target.files[0]); className="w-full h-24 object-cover rounded-lg cursor-pointer hover:opacity-80 transition-opacity"
if (image) { onClick={() => setSelectedImage(`/uploads/images/${image.filename}`)}
const updatedImages = [...(openNote.images || []), image]; />
setOpenNote({ ...openNote, images: updatedImages }); <Button
await handleUpdateNote({ images: updatedImages }); variant="destructive"
showSuccess("Image ajoutée"); size="icon"
} className="absolute top-1 right-1 h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
} catch (error) { onClick={async () => {
showError("Erreur lors de l'upload de l'image"); if (image.id && openNote.id) {
} try {
e.target.value = ''; const success = await NotesService.deleteImage(openNote.id, image.id);
} if (success) {
}} const updatedImages = (openNote.images || []).filter((_, i) => i !== index);
className="cursor-pointer" await handleUpdateNote({ images: updatedImages });
/> showSuccess("Image supprimée");
</div>
<div className="grid grid-cols-3 gap-4">
{openNote.images && openNote.images.length > 0 ? (
openNote.images.map((image, index) => (
<div key={index} className="relative group">
<img
src={`/uploads/images/${image.filename}`}
alt={image.original_name}
className="w-full h-32 object-cover rounded-lg"
/>
<Button
variant="destructive"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={async () => {
if (image.id && openNote.id) {
try {
const success = await NotesService.deleteImage(openNote.id, image.id);
if (success) {
const updatedImages = (openNote.images || []).filter((_, i) => i !== index);
await handleUpdateNote({ images: updatedImages });
}
} catch (error) {
showError("Erreur lors de la suppression");
}
} }
}} } catch (error) {
> showError("Erreur lors de la suppression");
<Trash2 className="h-4 w-4" /> }
</Button> }
</div> }}
)) >
) : ( <Trash2 className="h-3 w-3" />
<p className="col-span-3 text-center text-muted-foreground py-8">Aucune image</p> </Button>
)} </div>
</div> ))}
</TabsContent> </div>
)}
<TabsContent value="files" className="mt-4 space-y-4"> {/* Files */}
<div> {openNote.files && openNote.files.length > 0 && (
<Input <Card>
type="file" <CardContent className="p-4">
onChange={handleFileUpload} <div className="space-y-2">
className="cursor-pointer" {openNote.files.map((file: any, index: number) => (
/> <div key={index} className="flex items-center gap-3 p-2 border rounded group">
</div> <Paperclip className="h-4 w-4 text-muted-foreground" />
<div className="space-y-2">
{openNote.files && openNote.files.length > 0 ? (
openNote.files.map((file: any, index: number) => (
<div key={index} className="flex items-center gap-3 p-3 border rounded-lg group">
<Paperclip className="h-5 w-5 text-muted-foreground" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium truncate">{file.original_name || file.filename}</p> <p className="font-medium truncate text-sm">{file.original_name || file.filename}</p>
{file.size && ( {file.size && (
<p className="text-sm text-muted-foreground"> <p className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(2)} KB {(file.size / 1024).toFixed(2)} KB
</p> </p>
)} )}
</div> </div>
<div className="flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex gap-1">
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-7 w-7"
onClick={() => window.open(`/uploads/files/${file.filename}`, '_blank')} onClick={() => window.open(`/uploads/files/${file.filename}`, '_blank')}
> >
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-3 w-3" />
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
size="icon" size="icon"
className="h-7 w-7"
onClick={async () => { onClick={async () => {
if (file.id && openNote.id) { if (file.id && openNote.id) {
try { try {
@@ -1093,6 +1112,7 @@ const Index = () => {
if (response.ok) { if (response.ok) {
const updatedFiles = (openNote.files || []).filter((_, i) => i !== index); const updatedFiles = (openNote.files || []).filter((_, i) => i !== index);
await handleUpdateNote({ files: updatedFiles }); await handleUpdateNote({ files: updatedFiles });
showSuccess("Fichier supprimé");
} }
} catch (error) { } catch (error) {
showError("Erreur lors de la suppression"); showError("Erreur lors de la suppression");
@@ -1100,48 +1120,21 @@ const Index = () => {
} }
}} }}
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</div> </div>
</div> </div>
)) ))}
) : ( </div>
<p className="text-center text-muted-foreground py-8">Aucun fichier</p> </CardContent>
)} </Card>
</div> )}
</TabsContent>
<TabsContent value="tags" className="mt-4 space-y-4"> {/* Rich text editor */}
<Button <RichTextEditor
onClick={() => setAddTagModal(true)} content={openNote.content || ""}
className="w-full" onChange={handleContentChange}
> />
<Plus className="h-4 w-4 mr-2" />
Ajouter un tag
</Button>
<div className="flex gap-2 flex-wrap">
{noteTags.length > 0 ? (
noteTags.map((tag) => (
<Badge
key={tag.id}
variant="secondary"
className="text-base px-3 py-2 gap-2 cursor-pointer hover:bg-destructive hover:text-destructive-foreground transition-colors group"
>
<TagIcon className="h-4 w-4" />
{tag.tag}
<X
className="h-4 w-4 opacity-50 group-hover:opacity-100"
onClick={() => tag.id && handleDeleteTag(tag.id)}
/>
</Badge>
))
) : (
<p className="text-center text-muted-foreground py-8 w-full">Aucun tag</p>
)}
</div>
</TabsContent>
</Tabs>
{/* Note metadata */} {/* Note metadata */}
<Card className="mt-4"> <Card className="mt-4">
@@ -1913,6 +1906,31 @@ const Index = () => {
<div className="fixed bottom-4 right-4"> <div className="fixed bottom-4 right-4">
<MadeWithDyad /> <MadeWithDyad />
</div> </div>
{/* Image modal for fullscreen view */}
{selectedImage && (
<div
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
onClick={() => setSelectedImage(null)}
>
<div className="relative max-w-7xl max-h-full">
<Button
variant="ghost"
size="icon"
className="absolute -top-12 right-0 text-white hover:bg-white/20"
onClick={() => setSelectedImage(null)}
>
<X className="h-6 w-6" />
</Button>
<img
src={selectedImage}
alt="Preview"
className="max-w-full max-h-[90vh] object-contain rounded-lg"
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
)}
</div> </div>
); );
}; };