Feature: Redesign de l'interface sans sidebar pour écran 1920x1080

- Suppression de la sidebar : les notes sont affichées en grille 4 colonnes
- Notes s'ouvrent en dialog plein écran au clic
- Boxes Todos et RSS élargies et côte à côte en haut de page
- Optimisation de l'affichage pour résolution 1920x1080
- Ajout du changement de mot de passe dans l'administration
- Ajout du filtre Actives/Archivées pour les notes
- Amélioration des transitions et effets hover sur les cards
This commit is contained in:
Claude committed 2025-11-12 08:30:41 +00:00
1 parent f0c68b9db1
commit 3ac2cc0810
1 file changed
+295 -255
+295 -255
View File
@@ -8,13 +8,12 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { RichTextEditor } from "@/components/RichTextEditor"; import { RichTextEditor } from "@/components/RichTextEditor";
import { MadeWithDyad } from "@/components/made-with-dyad"; import { MadeWithDyad } from "@/components/made-with-dyad";
import { import {
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
Image, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X Image, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit
} from "lucide-react"; } from "lucide-react";
import AuthService from "@/services/AuthService"; import AuthService from "@/services/AuthService";
import AdminService from "@/services/AdminService"; import AdminService from "@/services/AdminService";
@@ -34,16 +33,17 @@ interface UserType {
const Index = () => { const Index = () => {
const [user, setUser] = useState<UserType | null>(null); const [user, setUser] = useState<UserType | null>(null);
const [showAdmin, setShowAdmin] = useState(false); const [showAdmin, setShowAdmin] = useState(false);
const [activeTab, setActiveTab] = useState("all");
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [notes, setNotes] = useState<Note[]>([]); const [notes, setNotes] = useState<Note[]>([]);
const [currentNote, setCurrentNote] = useState<Note | null>(null); const [currentNote, setCurrentNote] = useState<Note | null>(null);
const [showNoteDialog, setShowNoteDialog] = useState(false);
const [users, setUsers] = useState<UserType[]>([]); const [users, setUsers] = useState<UserType[]>([]);
const [todos, setTodos] = useState<Todo[]>([]); const [todos, setTodos] = useState<Todo[]>([]);
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]); const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]); const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
const [settings, setSettings] = useState<AppSettings>({}); const [settings, setSettings] = useState<AppSettings>({});
const [adminTab, setAdminTab] = useState("users"); const [adminTab, setAdminTab] = useState("users");
const [showArchived, setShowArchived] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
@@ -64,9 +64,6 @@ const Index = () => {
const fetchedNotes = await NotesService.getNotes(); const fetchedNotes = await NotesService.getNotes();
if (Array.isArray(fetchedNotes)) { if (Array.isArray(fetchedNotes)) {
setNotes(fetchedNotes); setNotes(fetchedNotes);
if (fetchedNotes.length > 0) {
setCurrentNote(fetchedNotes[0]);
}
} }
} catch (error) { } catch (error) {
console.error("Erreur lors du chargement des notes:", error); console.error("Erreur lors du chargement des notes:", error);
@@ -97,7 +94,7 @@ const Index = () => {
const loadRssArticles = async () => { const loadRssArticles = async () => {
try { try {
const articles = await RssService.getArticles(10); const articles = await RssService.getArticles(15);
if (Array.isArray(articles)) { if (Array.isArray(articles)) {
setRssArticles(articles); setRssArticles(articles);
} }
@@ -148,12 +145,18 @@ const Index = () => {
if (newNote) { if (newNote) {
setNotes([newNote, ...notes]); setNotes([newNote, ...notes]);
setCurrentNote(newNote); setCurrentNote(newNote);
setShowNoteDialog(true);
} }
} catch (error) { } catch (error) {
showError("Erreur lors de la création de la note"); showError("Erreur lors de la création de la note");
} }
}; };
const handleOpenNote = (note: Note) => {
setCurrentNote(note);
setShowNoteDialog(true);
};
const handleUpdateNote = async (updatedFields: Partial<Note>) => { const handleUpdateNote = async (updatedFields: Partial<Note>) => {
if (!currentNote) return; if (!currentNote) return;
@@ -187,7 +190,8 @@ const Index = () => {
if (success) { if (success) {
const updatedNotes = notes.filter(note => note.id !== currentNote.id); const updatedNotes = notes.filter(note => note.id !== currentNote.id);
setNotes(updatedNotes); setNotes(updatedNotes);
setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null); setShowNoteDialog(false);
setCurrentNote(null);
showSuccess("Note supprimée"); showSuccess("Note supprimée");
} }
} catch (error) { } catch (error) {
@@ -299,6 +303,20 @@ const Index = () => {
} }
}; };
const handleChangePassword = async (userId: number) => {
const newPassword = prompt("Nouveau mot de passe:");
if (newPassword) {
try {
const success = await AdminService.updateUserPassword(userId, newPassword);
if (success) {
showSuccess("Mot de passe modifié");
}
} catch (error) {
showError("Erreur lors de la modification");
}
}
};
const handleSaveSettings = async () => { const handleSaveSettings = async () => {
try { try {
await SettingsService.updateSettings(settings); await SettingsService.updateSettings(settings);
@@ -337,7 +355,7 @@ const Index = () => {
}; };
const filteredNotes = notes.filter(note => const filteredNotes = notes.filter(note =>
(activeTab === "all" || (activeTab === "archived" && note.archived)) && (showArchived ? note.archived : !note.archived) &&
(searchQuery === "" || (searchQuery === "" ||
(note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) || (note.title && note.title.toLowerCase().includes(searchQuery.toLowerCase())) ||
(note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase()))) (note.content && note.content.toLowerCase().includes(searchQuery.toLowerCase())))
@@ -347,21 +365,26 @@ const Index = () => {
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Top Navigation */} {/* Top Navigation */}
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-30"> <header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-30">
<div className="max-w-screen-2xl mx-auto px-4"> <div className="max-w-[1920px] mx-auto px-8">
<div className="flex h-16 items-center justify-between"> <div className="flex h-16 items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className="h-8 w-8 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-4 w-4 text-primary" /> <FileText className="h-5 w-5 text-primary" />
</div> </div>
<span className="text-xl font-semibold">NoteFlow</span> <span className="text-2xl font-semibold">NoteFlow</span>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-6">
<Button onClick={handleCreateNote} size="lg" className="gap-2">
<PlusCircle className="h-5 w-5" />
Nouvelle Note
</Button>
{user && ( {user && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-2"> <Button variant="ghost" className="flex items-center gap-2">
<Avatar className="h-8 w-8"> <Avatar className="h-9 w-9">
<AvatarFallback className="bg-primary/10 text-primary"> <AvatarFallback className="bg-primary/10 text-primary">
{user.username ? user.username.charAt(0).toUpperCase() : "U"} {user.username ? user.username.charAt(0).toUpperCase() : "U"}
</AvatarFallback> </AvatarFallback>
@@ -392,67 +415,215 @@ const Index = () => {
</header> </header>
{/* Main Content */} {/* Main Content */}
<div className="max-w-screen-2xl mx-auto px-4 py-6"> <div className="max-w-[1920px] mx-auto px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-[350px_1fr_300px] gap-6"> {/* Todos & RSS Boxes - Large side by side */}
{/* Left Sidebar - Notes */} <div className="grid grid-cols-2 gap-6 mb-8">
{/* Todos 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">
<CheckSquare className="h-6 w-6" />
Tâches
</CardTitle>
<Button size="lg" variant="outline" onClick={handleAddTodo} className="gap-2">
<Plus className="h-5 w-5" />
Ajouter
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="active" className="w-full">
<TabsList className="grid grid-cols-2 w-full">
<TabsTrigger value="active">Actives ({todos.filter(t => !t.completed).length})</TabsTrigger>
<TabsTrigger value="completed">Complétées ({todos.filter(t => t.completed).length})</TabsTrigger>
</TabsList>
<TabsContent value="active" className="mt-4">
<div className="space-y-3 max-h-[400px] overflow-y-auto">
{todos.filter(t => !t.completed).length > 0 ? (
todos.filter(t => !t.completed).map(todo => (
<div key={todo.id} className="flex items-center gap-3 p-3 border rounded-lg hover:bg-accent/50 transition-colors group">
<Checkbox
checked={false}
onCheckedChange={() => todo.id && handleToggleTodo(todo.id)}
className="h-5 w-5"
/>
<span className="text-base flex-1">
{todo.text}
</span>
<Button
variant="ghost"
size="icon"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => todo.id && handleDeleteTodo(todo.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))
) : (
<p className="text-center text-muted-foreground py-8">Aucune tâche active</p>
)}
</div>
</TabsContent>
<TabsContent value="completed" className="mt-4">
<div className="space-y-3 max-h-[400px] overflow-y-auto">
{todos.filter(t => t.completed).length > 0 ? (
todos.filter(t => t.completed).map(todo => (
<div key={todo.id} className="flex items-center gap-3 p-3 border rounded-lg hover:bg-accent/50 transition-colors group">
<Checkbox
checked={true}
onCheckedChange={() => todo.id && handleToggleTodo(todo.id)}
className="h-5 w-5"
/>
<span className="text-base flex-1 line-through text-muted-foreground">
{todo.text}
</span>
<Button
variant="ghost"
size="icon"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => todo.id && handleDeleteTodo(todo.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))
) : (
<p className="text-center text-muted-foreground py-8">Aucune tâche complétée</p>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* RSS 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">
<Rss className="h-6 w-6" />
Flux RSS
</CardTitle>
<div className="flex gap-2">
<Button size="lg" variant="outline" onClick={handleRefreshRss} className="gap-2">
<RefreshCw className="h-5 w-5" />
Actualiser
</Button>
<Button size="lg" variant="outline" onClick={handleAddRssFeed} className="gap-2">
<Plus className="h-5 w-5" />
Ajouter
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3 max-h-[480px] overflow-y-auto">
{rssArticles.length > 0 ? (
rssArticles.map(article => (
<div
key={article.id}
className="p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
onClick={() => article.link && window.open(article.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">{article.title}</h4>
{article.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{article.description.replace(/<[^>]*>/g, '')}
</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 article
<br />
<Button
variant="link"
className="mt-2"
onClick={handleAddRssFeed}
>
Ajouter un flux RSS
</Button>
</p>
)}
</div>
</CardContent>
</Card>
</div>
{/* Notes Section */}
<div className="space-y-6"> <div className="space-y-6">
<div className="relative"> <div className="flex items-center justify-between">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <div className="flex items-center gap-4">
<h2 className="text-2xl font-semibold">Mes Notes</h2>
<div className="flex gap-2">
<Button
variant={!showArchived ? "default" : "outline"}
onClick={() => setShowArchived(false)}
>
Actives ({notes.filter(n => !n.archived).length})
</Button>
<Button
variant={showArchived ? "default" : "outline"}
onClick={() => setShowArchived(true)}
>
Archivées ({notes.filter(n => n.archived).length})
</Button>
</div>
</div>
<div className="relative w-96">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
<Input <Input
type="search" type="search"
placeholder="Rechercher une note..." placeholder="Rechercher dans les notes..."
className="pl-9" className="pl-10 h-12 text-base"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
</div> </div>
</div>
<Button {/* Notes Grid */}
className="w-full flex items-center gap-2" <div className="grid grid-cols-4 gap-6">
size="lg" {filteredNotes.length > 0 ? (
onClick={handleCreateNote}
>
<PlusCircle className="h-4 w-4" />
Nouvelle Note
</Button>
<Tabs defaultValue="all" value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-2 w-full">
<TabsTrigger value="all">Toutes</TabsTrigger>
<TabsTrigger value="archived">Archivées</TabsTrigger>
</TabsList>
</Tabs>
<div className="space-y-2 max-h-[calc(100vh-340px)] overflow-y-auto">
{filteredNotes.length === 0 ? (
<p className="text-center text-muted-foreground py-4">Aucune note trouvée</p>
) : (
filteredNotes.map(note => ( filteredNotes.map(note => (
<Card <Card
key={note.id} key={note.id}
className={`cursor-pointer hover:shadow-md transition-shadow ${currentNote?.id === note.id ? 'border-primary' : ''}`} className="cursor-pointer hover:shadow-xl transition-all hover:scale-105 group"
onClick={() => setCurrentNote(note)} onClick={() => handleOpenNote(note)}
> >
<CardContent className="p-4"> <CardContent className="p-6">
<h3 className="font-medium truncate">{note.title || "Sans titre"}</h3> <h3 className="font-semibold text-lg mb-3 line-clamp-2 min-h-[56px]">
<p className="text-sm text-muted-foreground line-clamp-2 mt-1"> {note.title || "Sans titre"}
{note.content ? note.content.replace(/<[^>]*>/g, '') : ""} </h3>
<p className="text-sm text-muted-foreground line-clamp-4 mb-4 min-h-[80px]">
{note.content ? note.content.replace(/<[^>]*>/g, '') : "Note vide"}
</p> </p>
<div className="flex gap-2 mt-2"> <div className="flex gap-2 flex-wrap">
{note.todos && note.todos.length > 0 && ( {note.todos && note.todos.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary">
<CheckSquare className="h-3 w-3 mr-1" /> <CheckSquare className="h-3 w-3 mr-1" />
{note.todos.length} {note.todos.length}
</Badge> </Badge>
)} )}
{note.images && note.images.length > 0 && ( {note.images && note.images.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary">
<Image className="h-3 w-3 mr-1" /> <Image className="h-3 w-3 mr-1" />
{note.images.length} {note.images.length}
</Badge> </Badge>
)} )}
{note.files && note.files.length > 0 && ( {note.files && note.files.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary">
<Paperclip className="h-3 w-3 mr-1" /> <Paperclip className="h-3 w-3 mr-1" />
{note.files.length} {note.files.length}
</Badge> </Badge>
@@ -461,19 +632,34 @@ const Index = () => {
</CardContent> </CardContent>
</Card> </Card>
)) ))
) : (
<div className="col-span-4 text-center py-16">
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<p className="text-muted-foreground text-lg mb-4">
{searchQuery ? "Aucune note trouvée" : "Aucune note"}
</p>
{!searchQuery && (
<Button onClick={handleCreateNote} size="lg">
<Plus className="h-5 w-5 mr-2" />
Créer votre première note
</Button>
)}
</div>
)} )}
</div> </div>
</div> </div>
</div>
{/* Center - Note Editor */} {/* Note Editor Dialog - Full Screen */}
{currentNote ? ( <Dialog open={showNoteDialog} onOpenChange={setShowNoteDialog}>
<div className="space-y-4"> <DialogContent className="max-w-[95vw] max-h-[95vh] overflow-y-auto">
<div className="flex items-center justify-between"> <DialogHeader>
<div className="flex items-center justify-between pr-8">
<Input <Input
type="text" type="text"
placeholder="Titre de la note" placeholder="Titre de la note"
className="text-xl font-semibold border-none shadow-none text-2xl h-auto px-0 focus-visible:ring-0" className="text-2xl font-semibold border-none shadow-none h-auto px-0 focus-visible:ring-0"
value={currentNote.title || ""} value={currentNote?.title || ""}
onChange={(e) => handleUpdateNote({ title: e.target.value })} onChange={(e) => handleUpdateNote({ title: e.target.value })}
/> />
@@ -481,7 +667,8 @@ const Index = () => {
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={() => handleUpdateNote({ archived: !currentNote.archived })} onClick={() => currentNote && handleUpdateNote({ archived: !currentNote.archived })}
title={currentNote?.archived ? "Désarchiver" : "Archiver"}
> >
<Archive className="h-4 w-4" /> <Archive className="h-4 w-4" />
</Button> </Button>
@@ -489,14 +676,17 @@ const Index = () => {
variant="outline" variant="outline"
size="icon" size="icon"
onClick={handleDeleteNote} onClick={handleDeleteNote}
title="Supprimer"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div> </div>
</DialogHeader>
<Tabs defaultValue="content"> {currentNote && (
<TabsList> <Tabs defaultValue="content" className="mt-4">
<TabsList className="grid grid-cols-4 w-full max-w-md">
<TabsTrigger value="content">Contenu</TabsTrigger> <TabsTrigger value="content">Contenu</TabsTrigger>
<TabsTrigger value="todos"> <TabsTrigger value="todos">
Tâches Tâches
@@ -518,7 +708,7 @@ const Index = () => {
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="content" className="mt-4"> <TabsContent value="content" className="mt-6">
<RichTextEditor <RichTextEditor
content={currentNote.content || ""} content={currentNote.content || ""}
onChange={(content) => handleUpdateNote({ content })} onChange={(content) => handleUpdateNote({ content })}
@@ -526,13 +716,12 @@ const Index = () => {
/> />
</TabsContent> </TabsContent>
<TabsContent value="todos" className="mt-4"> <TabsContent value="todos" className="mt-6">
<Card> <Card>
<CardContent className="p-4 space-y-4"> <CardContent className="p-6 space-y-4">
<Button <Button
variant="outline" variant="outline"
size="sm" className="w-full"
className="flex items-center gap-2"
onClick={() => { onClick={() => {
const text = prompt("Nouvelle tâche:"); const text = prompt("Nouvelle tâche:");
if (text) { if (text) {
@@ -541,14 +730,14 @@ const Index = () => {
} }
}} }}
> >
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4 mr-2" />
Ajouter une tâche Ajouter une tâche
</Button> </Button>
<div className="space-y-2"> <div className="space-y-2">
{currentNote.todos && currentNote.todos.length > 0 ? ( {currentNote.todos && currentNote.todos.length > 0 ? (
currentNote.todos.map((todo, index) => ( currentNote.todos.map((todo, index) => (
<div key={index} className="flex items-center gap-2 p-2 border rounded-md"> <div key={index} className="flex items-center gap-2 p-3 border rounded-md">
<Checkbox <Checkbox
checked={todo.completed} checked={todo.completed}
onCheckedChange={() => { onCheckedChange={() => {
@@ -563,7 +752,6 @@ const Index = () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6"
onClick={() => { onClick={() => {
const updatedTodos = [...(currentNote.todos || [])]; const updatedTodos = [...(currentNote.todos || [])];
updatedTodos.splice(index, 1); updatedTodos.splice(index, 1);
@@ -582,18 +770,17 @@ const Index = () => {
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="images" className="mt-4"> <TabsContent value="images" className="mt-6">
<Card> <Card>
<CardContent className="p-4 space-y-4"> <CardContent className="p-6 space-y-4">
<label htmlFor="image-upload"> <label htmlFor="image-upload">
<Button <Button
variant="outline" variant="outline"
size="sm" className="w-full"
className="flex items-center gap-2"
type="button" type="button"
onClick={() => document.getElementById('image-upload')?.click()} onClick={() => document.getElementById('image-upload')?.click()}
> >
<Image className="h-4 w-4" /> <Image className="h-4 w-4 mr-2" />
Ajouter une image Ajouter une image
</Button> </Button>
<input <input
@@ -620,7 +807,7 @@ const Index = () => {
/> />
</label> </label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-4 gap-4">
{currentNote.images && currentNote.images.length > 0 ? ( {currentNote.images && currentNote.images.length > 0 ? (
currentNote.images.map((image, index) => ( currentNote.images.map((image, index) => (
<div key={index} className="relative aspect-square rounded-md overflow-hidden border group"> <div key={index} className="relative aspect-square rounded-md overflow-hidden border group">
@@ -632,7 +819,7 @@ const Index = () => {
<Button <Button
variant="destructive" variant="destructive"
size="icon" size="icon"
className="absolute top-1 right-1 h-6 w-6 rounded-full opacity-0 group-hover:opacity-100 transition-opacity" className="absolute top-2 right-2 h-8 w-8 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
onClick={async () => { onClick={async () => {
if (image.id && currentNote.id) { if (image.id && currentNote.id) {
try { try {
@@ -648,30 +835,29 @@ const Index = () => {
} }
}} }}
> >
<X className="h-3 w-3" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
)) ))
) : ( ) : (
<p className="col-span-full text-center text-muted-foreground py-8">Aucune image</p> <p className="col-span-4 text-center text-muted-foreground py-8">Aucune image</p>
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
<TabsContent value="files" className="mt-4"> <TabsContent value="files" className="mt-6">
<Card> <Card>
<CardContent className="p-4 space-y-4"> <CardContent className="p-6 space-y-4">
<label htmlFor="file-upload"> <label htmlFor="file-upload">
<Button <Button
variant="outline" variant="outline"
size="sm" className="w-full"
className="flex items-center gap-2"
type="button" type="button"
onClick={() => document.getElementById('file-upload')?.click()} onClick={() => document.getElementById('file-upload')?.click()}
> >
<Paperclip className="h-4 w-4" /> <Paperclip className="h-4 w-4 mr-2" />
Ajouter un fichier Ajouter un fichier
</Button> </Button>
<input <input
@@ -686,18 +872,17 @@ const Index = () => {
{currentNote.files && currentNote.files.length > 0 ? ( {currentNote.files && currentNote.files.length > 0 ? (
currentNote.files.map((file: any, index: number) => ( currentNote.files.map((file: any, index: number) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-md hover:bg-accent/50 transition-colors group"> <div key={index} className="flex items-center justify-between p-3 border rounded-md hover:bg-accent/50 transition-colors group">
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-3 flex-1">
<Paperclip className="h-4 w-4 text-muted-foreground" /> <Paperclip className="h-5 w-5 text-muted-foreground" />
<span className="text-sm truncate">{file.filename || file.original_name}</span> <span className="truncate">{file.filename || file.original_name}</span>
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary">
{file.size ? `${(file.size / 1024).toFixed(1)} KB` : ''} {file.size ? `${(file.size / 1024).toFixed(1)} KB` : ''}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-2">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8"
onClick={() => window.open(`/uploads/${file.filename}`, '_blank')} onClick={() => window.open(`/uploads/${file.filename}`, '_blank')}
> >
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
@@ -705,7 +890,7 @@ const Index = () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity" className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={async () => { onClick={async () => {
if (file.id && currentNote.id) { if (file.id && currentNote.id) {
try { try {
@@ -739,164 +924,9 @@ const Index = () => {
</Card> </Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div>
) : (
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<div className="text-center">
<FileText className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<p className="text-muted-foreground mb-4">Aucune note sélectionnée</p>
<Button onClick={handleCreateNote}>
<Plus className="h-4 w-4 mr-2" />
Créer une note
</Button>
</div>
</div>
)} )}
</DialogContent>
{/* Right Sidebar - Todos & RSS */} </Dialog>
<div className="space-y-6">
{/* Boxes côte à côte */}
<div className="grid grid-cols-2 gap-4">
{/* Todos Box */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<CheckSquare className="h-4 w-4" />
Tâches
</CardTitle>
<Button size="sm" variant="ghost" onClick={handleAddTodo} className="h-7 w-7 p-0">
<Plus className="h-3 w-3" />
</Button>
</div>
</CardHeader>
<CardContent className="pb-3">
<Tabs defaultValue="active" className="w-full">
<TabsList className="grid grid-cols-2 w-full h-8">
<TabsTrigger value="active" className="text-xs">Actives</TabsTrigger>
<TabsTrigger value="completed" className="text-xs">Complétées</TabsTrigger>
</TabsList>
<TabsContent value="active" className="mt-2">
<div className="space-y-2 max-h-[250px] overflow-y-auto">
{todos.filter(t => !t.completed).length > 0 ? (
todos.filter(t => !t.completed).map(todo => (
<div key={todo.id} className="flex items-center gap-2 p-2 border rounded-md hover:bg-accent/50 transition-colors group">
<Checkbox
checked={false}
onCheckedChange={() => todo.id && handleToggleTodo(todo.id)}
/>
<span className="text-xs flex-1">
{todo.text}
</span>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => todo.id && handleDeleteTodo(todo.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))
) : (
<p className="text-xs text-center text-muted-foreground py-4">Aucune tâche active</p>
)}
</div>
</TabsContent>
<TabsContent value="completed" className="mt-2">
<div className="space-y-2 max-h-[250px] overflow-y-auto">
{todos.filter(t => t.completed).length > 0 ? (
todos.filter(t => t.completed).map(todo => (
<div key={todo.id} className="flex items-center gap-2 p-2 border rounded-md hover:bg-accent/50 transition-colors group">
<Checkbox
checked={true}
onCheckedChange={() => todo.id && handleToggleTodo(todo.id)}
/>
<span className="text-xs flex-1 line-through text-muted-foreground">
{todo.text}
</span>
<Button
variant="ghost"
size="icon"
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => todo.id && handleDeleteTodo(todo.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))
) : (
<p className="text-xs text-center text-muted-foreground py-4">Aucune tâche complétée</p>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* RSS Box */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Rss className="h-4 w-4" />
Flux RSS
</CardTitle>
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={handleRefreshRss} className="h-7 w-7 p-0">
<RefreshCw className="h-3 w-3" />
</Button>
<Button size="sm" variant="ghost" onClick={handleAddRssFeed} className="h-7 w-7 p-0">
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pb-3">
<div className="space-y-2 max-h-[330px] overflow-y-auto">
{rssArticles.length > 0 ? (
rssArticles.map(article => (
<div
key={article.id}
className="p-2 border rounded-md hover:bg-accent/50 transition-colors cursor-pointer"
onClick={() => article.link && window.open(article.link, '_blank')}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h4 className="text-xs font-medium line-clamp-2 mb-1">{article.title}</h4>
{article.description && (
<p className="text-xs text-muted-foreground line-clamp-2">
{article.description.replace(/<[^>]*>/g, '')}
</p>
)}
</div>
<ExternalLink className="h-3 w-3 text-muted-foreground flex-shrink-0 mt-1" />
</div>
</div>
))
) : (
<p className="text-xs text-center text-muted-foreground py-4">
Aucun article
<br />
<Button
variant="link"
size="sm"
className="mt-2 text-xs"
onClick={handleAddRssFeed}
>
Ajouter un flux RSS
</Button>
</p>
)}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
{/* Admin Modal */} {/* Admin Modal */}
<Dialog open={showAdmin} onOpenChange={setShowAdmin}> <Dialog open={showAdmin} onOpenChange={setShowAdmin}>
@@ -926,29 +956,39 @@ const Index = () => {
</Button> </Button>
<div className="space-y-2"> <div className="space-y-2">
{users.map(user => ( {users.map(u => (
<Card key={user.id}> <Card key={u.id}>
<CardContent className="p-4 flex items-center justify-between"> <CardContent className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar> <Avatar>
<AvatarFallback>{user.username.charAt(0).toUpperCase()}</AvatarFallback> <AvatarFallback>{u.username.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<p className="font-medium">{user.username}</p> <p className="font-medium">{u.username}</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{user.is_admin ? "Administrateur" : "Utilisateur"} {u.is_admin ? "Administrateur" : "Utilisateur"}
</p> </p>
</div> </div>
</div> </div>
{user.id !== 1 && ( <div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleChangePassword(u.id)}
>
<Key className="h-4 w-4 mr-2" />
Mot de passe
</Button>
{u.id !== 1 && (
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={() => handleDeleteUser(user.id)} onClick={() => handleDeleteUser(u.id)}
> >
Supprimer Supprimer
</Button> </Button>
)} )}
</div>
</CardContent> </CardContent>
</Card> </Card>
))} ))}